Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions .agents/skills/add-command/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: add-command
description: Guide for adding new CLI commands or subcommands to twist-cli. Use when implementing new SDK endpoints, adding subcommands to existing command groups, or extending CLI functionality.
description: Guide for adding new CLI commands or subcommands to comms-cli. Use when implementing new SDK endpoints, adding subcommands to existing command groups, or extending CLI functionality.
---

# Adding a New CLI Command or Subcommand
Expand All @@ -19,7 +19,7 @@ Color convention:

## 2. Read-Only Permissions (`src/lib/permissions.ts`)

If the new command uses a **read-only** SDK method (e.g., `getXxx`, `listXxx`), add it to the `KNOWN_SAFE_API_METHODS` set. This set uses a default-deny approach: any method **not** listed is treated as mutating and will be blocked when the CLI is authenticated with a read-only OAuth token (`tw auth login --read-only`).
If the new command uses a **read-only** SDK method (e.g., `getXxx`, `listXxx`), add it to the `KNOWN_SAFE_API_METHODS` set. This set uses a default-deny approach: any method **not** listed is treated as mutating and will be blocked when the CLI is authenticated with a read-only OAuth token (`cm auth login --read-only`).

- **Read-only methods** (fetch/list/view): add to `KNOWN_SAFE_API_METHODS`
- **Mutating methods** (create/update/delete/archive/mute): do NOT add — they are blocked by default, which is the correct behavior
Expand Down Expand Up @@ -60,7 +60,7 @@ Single-subcommand commands (e.g., `channel.ts`, `inbox.ts`) remain as flat files

### ID resolution

- `resolveThreadId(ref)` — resolve thread by numeric ID or Twist URL
- `resolveThreadId(ref)` — resolve thread by numeric ID or Comms URL
- `resolveChannelId(ref)` — resolve channel by numeric ID, URL, or fuzzy name
- `resolveWorkspaceRef(ref)` — resolve workspace by ID or fuzzy name
- `resolveConversationId(ref)` — resolve conversation by numeric ID or URL
Expand Down Expand Up @@ -89,7 +89,7 @@ The variable assignment (`const myCmd = ...`) is needed so the `.action()` callb

### Implicit view subcommand

For entity commands with a `view` subcommand, mark it as the default so `tw thread 123` maps to `tw thread view 123`:
For entity commands with a `view` subcommand, mark it as the default so `cm thread 123` maps to `cm thread view 123`:

```typescript
thread
Expand Down Expand Up @@ -128,7 +128,7 @@ const commands: Record<string, [string, () => Promise<(p: Command) => void>]> =

## 4. Accessibility (`src/lib/output.ts`)

The CLI supports accessible mode via `isAccessible()` (checks `TW_ACCESSIBLE=1` or `--accessible` flag). When adding output that uses color or visual elements, consider whether information is conveyed **only** by color or decoration.
The CLI supports accessible mode via `isAccessible()` (checks `CM_ACCESSIBLE=1` or `--accessible` flag). When adding output that uses color or visual elements, consider whether information is conveyed **only** by color or decoration.

### When to add accessible alternatives

Expand Down Expand Up @@ -160,12 +160,12 @@ Tests mock the API layer directly using `vi.mock` and `vi.hoisted`. Follow the e

```typescript
const apiMocks = vi.hoisted(() => ({
getTwistClient: vi.fn(),
getCommsClient: vi.fn(),
}))

vi.mock('../lib/api.js', async (importOriginal) => ({
...(await importOriginal<typeof import('../lib/api.js')>()),
getTwistClient: apiMocks.getTwistClient,
getCommsClient: apiMocks.getCommsClient,
}))

vi.mock('../lib/markdown.js', () => ({
Expand Down Expand Up @@ -218,7 +218,7 @@ After all code changes are complete:
npm run build && npm run sync:skill
```

This builds the project and regenerates `skills/twist-cli/SKILL.md` from the compiled skill content. The regenerated file must be committed. CI will fail (`npm run check:skill-sync`) if it is out of sync.
This builds the project and regenerates `skills/comms-cli/SKILL.md` from the compiled skill content. The regenerated file must be committed. CI will fail (`npm run check:skill-sync`) if it is out of sync.

## 8. Verify

Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/add-command/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: add-command
description: Guide for adding new CLI commands or subcommands to twist-cli. Use when implementing new SDK endpoints, adding subcommands to existing command groups, or extending CLI functionality.
description: Guide for adding new CLI commands or subcommands to comms-cli. Use when implementing new SDK endpoints, adding subcommands to existing command groups, or extending CLI functionality.
---

See [/.agents/skills/add-command/SKILL.md](../../../.agents/skills/add-command/SKILL.md) for the full guide.
59 changes: 0 additions & 59 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,6 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Capture previous tag
if: github.ref_name == 'main'
id: previous_tag
run: echo "tag=$(git describe --tags --abbrev=0 --exclude='*-*' 2>/dev/null || true)" >> "$GITHUB_OUTPUT"

- name: Release
run: npx semantic-release
env:
Expand All @@ -81,57 +76,3 @@ jobs:
GIT_AUTHOR_EMAIL: ${{ steps.bot_user.outputs.id }}+${{ steps.generate_token.outputs.app-slug }}[bot]@users.noreply.github.com
GIT_COMMITTER_NAME: ${{ steps.generate_token.outputs.app-slug }}[bot]
GIT_COMMITTER_EMAIL: ${{ steps.bot_user.outputs.id }}+${{ steps.generate_token.outputs.app-slug }}[bot]@users.noreply.github.com

- name: Derive release announcement
if: github.ref_name == 'main'
id: announcement
env:
PREVIOUS_TAG: ${{ steps.previous_tag.outputs.tag }}
run: |
git fetch --force --tags origin

new_tag="$(git describe --tags --abbrev=0 2>/dev/null || true)"
if [ -z "${new_tag}" ] || [ "${new_tag}" = "${PREVIOUS_TAG}" ]; then
echo "should_announce=false" >> "$GITHUB_OUTPUT"
exit 0
fi

package_name="$(node -p "JSON.parse(require('fs').readFileSync('package.json', 'utf8')).name")"
package_version="$(node -p "JSON.parse(require('fs').readFileSync('package.json', 'utf8')).version")"
release_version="${new_tag#v}"

package_url="https://www.npmjs.com/package/${package_name}/v/${package_version}"
release_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases/tag/${new_tag}"
repo_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}"

if [ -n "${PREVIOUS_TAG}" ]; then
changelog="$(git log --no-merges --reverse --pretty='format:- %s (%H-%h)' "${PREVIOUS_TAG}..${new_tag}" | grep -v '^- chore(release): ' || true)"
else
changelog="$(git log --no-merges --reverse --pretty='format:- %s (%H-%h)' "${new_tag}" | grep -v '^- chore(release): ' || true)"
fi

if [ -z "${changelog}" ]; then
changelog='- No additional commits listed.'
else
changelog="$(printf '%s\n' "${changelog}" | sed -E -e 's,\(([a-f0-9]+)-([a-f0-9]+)\),([`\2`]('"${repo_url}"'/commit/\1)),g' | sed -E -e 's,\(#([0-9]+)\),([#\1]('"${repo_url}"'/pull/\1)),g')"
fi

{
echo "should_announce=true"
echo "message<<EOF"
echo "**Twist CLI ${new_tag} published** 🚀"
echo
printf '%s\n' "${changelog}"
echo
echo "[GitHub release](${release_url}) | [npm package](${package_url})"
echo "EOF"
} >> "$GITHUB_OUTPUT"

- name: Announce release in Twist
if: github.ref_name == 'main' && steps.announcement.outputs.should_announce == 'true'
uses: Doist/twist-post-action@74a0255b75ad93c06b9eb1009960106efe13f5ca
with:
message: ${{ steps.announcement.outputs.message }}
install_id: ${{ secrets.TWIST_RELEASE_INSTALL_ID }}
install_token: ${{ secrets.TWIST_RELEASE_INSTALL_TOKEN }}
continue-on-error: true
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Update Twist SDK
name: Update Comms SDK

on:
workflow_dispatch:
Expand All @@ -8,7 +8,7 @@ permissions:
pull-requests: write

jobs:
update-twist-sdk:
update-comms-sdk:
Comment thread
amix marked this conversation as resolved.
runs-on: ubuntu-latest
timeout-minutes: 10

Expand All @@ -27,8 +27,8 @@ jobs:
- name: Install current dependencies
run: npm ci

- name: Update @doist/twist-sdk to latest
run: npm install @doist/twist-sdk@latest
- name: Update @doist/comms-sdk to latest
run: npm install @doist/comms-sdk@latest

- name: Build
run: npm run build
Expand All @@ -46,10 +46,10 @@ jobs:
id: changes
run: |
if git diff --quiet package.json package-lock.json; then
echo "No updates available - @doist/twist-sdk is already at the latest version"
echo "No updates available - @doist/comms-sdk is already at the latest version"
echo "has_changes=false" >> $GITHUB_OUTPUT
else
echo "Changes detected - @doist/twist-sdk has been updated"
echo "Changes detected - @doist/comms-sdk has been updated"
echo "has_changes=true" >> $GITHUB_OUTPUT
fi

Expand All @@ -59,7 +59,7 @@ jobs:
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add package.json package-lock.json
git commit -m "fix(deps): update to latest \`@doist/twist-sdk\` package"
git commit -m "fix(deps): update to latest \`@doist/comms-sdk\` package"
git push

- name: Trigger release workflow
Expand All @@ -74,9 +74,9 @@ jobs:
- name: Report success
run: |
if [ "${{ steps.changes.outputs.has_changes }}" == "true" ]; then
echo "✅ Successfully updated @doist/twist-sdk package"
echo "✅ Successfully updated @doist/comms-sdk package"
echo "📦 Changes committed to main branch"
echo "🚀 Release workflow triggered - semantic release will create a new patch version"
else
echo "ℹ️ No updates needed - @doist/twist-sdk is already at the latest version"
echo "ℹ️ No updates needed - @doist/comms-sdk is already at the latest version"
fi
20 changes: 10 additions & 10 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,27 +36,27 @@ node dist/index.js <command>

## Architecture

This is a TypeScript CLI (`tw`) for Twist messaging, built with Commander.js.
This is a TypeScript CLI (`cm`) for Comms messaging, built with Commander.js.

**Entry point**: `src/index.ts` registers all commands with Commander.

**Commands** (`src/commands/`): Commands with multiple subcommands use a folder-based structure (`src/commands/<entity>/index.ts`) where the index file exports a `register*Command(program)` function and wires Commander subcommands to handler functions in sibling files. Single-command files remain as flat files (`src/commands/<entity>.ts`). Each subcommand handler file exports one async action function and imports from `../../lib/`. An optional `helpers.ts` holds shared constants/utilities used by multiple subcommands. Commands support `--json`, `--ndjson`, and `--full` flags for machine-readable output.

**Lib** (`src/lib/`):

- `api.ts` - Singleton TwistApi client from `@doist/twist-sdk`, workspace/user caching
- `refs.ts` - Reference parsing: accepts IDs (`id:123` or bare `123`), Twist URLs, or fuzzy names for workspaces/users
- `api.ts` - Singleton CommsApi client from `@doist/comms-sdk`, workspace/user caching
- `refs.ts` - Reference parsing: accepts IDs (`id:123` or bare `123`), Comms URLs, or fuzzy names for workspaces/users
- `output.ts` - JSON/NDJSON formatting with essential field filtering per entity type
- `config.ts` - Persists config to `~/.config/twist-cli/config.json`
- `config.ts` - Persists config to `~/.config/comms-cli/config.json`
- `auth.ts` - Token loading/saving/clearing (env var or config file)
- `markdown.ts` - Terminal markdown rendering via `marked` + `marked-terminal`
- `completion.ts` - Commander tree-walker + completion helpers for shell tab completion

**Reference system**: The CLI accepts flexible references throughout - numeric IDs, `id:` prefixed IDs, full Twist URLs (parsed via `parseTwistUrl`), or fuzzy name matching for workspaces/users.
**Reference system**: The CLI accepts flexible references throughout - numeric IDs, `id:` prefixed IDs, full Comms URLs (parsed via `parseCommsUrl`), or fuzzy name matching for workspaces/users.

## Key Patterns

- **Implicit view subcommand**: `tw thread <ref>` defaults to `tw thread view <ref>` via Commander's `{ isDefault: true }`. Same for `conversation` and `msg`. Edge case: if a ref matches a subcommand name (e.g., "reply"), the subcommand wins — user must use `tw thread view reply`
- **Implicit view subcommand**: `cm thread <ref>` defaults to `cm thread view <ref>` via Commander's `{ isDefault: true }`. Same for `conversation` and `msg`. Edge case: if a ref matches a subcommand name (e.g., "reply"), the subcommand wins — user must use `cm thread view reply`
- **Named flag aliases**: Where commands accept positional `[workspace-ref]`, the `--workspace` flag is also accepted. Error if both positional and flag are provided
- **JSON output on mutating commands**: Mutating commands (create, update, delete, archive) should support `--json` output where it provides scripting value. Commands that return an object from the API (create/update) should also support `--full`. Commands where the API returns void should output a minimal status object (e.g. `{ id, deleted: true }` or `{ id, isArchived: true }`). Extend `MutationOptions` in `src/lib/options.ts` (which already includes `json` and `full`) rather than adding these fields ad hoc. Use `formatJson()` from `src/lib/output.ts` for the output. See `src/commands/away.ts` as the reference implementation.
- **Spinner messages**: When adding new SDK method calls, add a corresponding entry in the `API_SPINNER_MESSAGES` map in `src/lib/api.ts`. Every user-facing API call should have a spinner message so the CLI shows progress feedback.
Expand Down Expand Up @@ -87,7 +87,7 @@ Lefthook runs type-check, oxlint, and oxfmt on pre-commit, tests on pre-push.

## Skill Content (Agent Command Reference)

The file `src/lib/skills/content.ts` exports `SKILL_CONTENT` — a comprehensive command reference that gets installed into AI agent skill directories via `tw skill install`. This is the source of truth that agents use to understand available CLI commands.
The file `src/lib/skills/content.ts` exports `SKILL_CONTENT` — a comprehensive command reference that gets installed into AI agent skill directories via `cm skill install`. This is the source of truth that agents use to understand available CLI commands.

**Whenever commands, subcommands, flags, or options are added, updated, or removed in `src/commands/`, the `SKILL_CONTENT` in `src/lib/skills/content.ts` must be updated to match.** This includes:

Expand All @@ -98,7 +98,7 @@ The file `src/lib/skills/content.ts` exports `SKILL_CONTENT` — a comprehensive

After updating `SKILL_CONTENT`:

1. Run `npm run build && npm run sync:skill` to regenerate `skills/twist-cli/SKILL.md` (the standalone skill file used by `npx skills add`)
2. Run `tw skill update claude-code` (and any other installed agents) to propagate changes to installed skill files
1. Run `npm run build && npm run sync:skill` to regenerate `skills/comms-cli/SKILL.md` (the standalone skill file used by `npx skills add`)
2. Run `cm skill update claude-code` (and any other installed agents) to propagate changes to installed skill files

A CI check (`npm run check:skill-sync`) runs on pull requests and will fail if `skills/twist-cli/SKILL.md` is out of sync with `content.ts`.
A CI check (`npm run check:skill-sync`) runs on pull requests and will fail if `skills/comms-cli/SKILL.md` is out of sync with `content.ts`.
14 changes: 7 additions & 7 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
# Contributing to Twist CLI
# Contributing to Comms CLI

The following is a set of guidelines for contributing to Twist CLI. Please read these guidelines before creating an issue or pull request.
The following is a set of guidelines for contributing to Comms CLI. Please read these guidelines before creating an issue or pull request.

## Open Development

All work on Twist CLI happens directly on [GitHub](https://github.com/Doist/twist-cli). Both core team members and external contributors send pull requests that go through the same review process.
All work on Comms CLI happens directly on [GitHub](https://github.com/Doist/comms-cli). Both core team members and external contributors send pull requests that go through the same review process.

## Semantic Versioning

Twist CLI follows [semantic versioning](https://semver.org/). We release patch versions for bugfixes, minor versions for new features or non-essential changes, and major versions for any breaking changes.
Comms CLI follows [semantic versioning](https://semver.org/). We release patch versions for bugfixes, minor versions for new features or non-essential changes, and major versions for any breaking changes.

Every significant change is documented in the [CHANGELOG.md](CHANGELOG.md) file.

## Branch Organization

Submit all changes to the [main](https://github.com/Doist/twist-cli/tree/main) branch (via PR) by default. For pre-release work, target the `next` branch instead — see [Release Process](#release-process-core-team-only) for details.
Submit all changes to the [main](https://github.com/Doist/comms-cli/tree/main) branch (via PR) by default. For pre-release work, target the `next` branch instead — see [Release Process](#release-process-core-team-only) for details.

We do our best to keep `main` in good shape, with all tests passing.

Expand Down Expand Up @@ -91,9 +91,9 @@ To test features before publishing a stable release:
### Installing a pre-release

```sh
npm install @doist/twist-cli@next
npm install @doist/comms-cli@next
```

## License

By contributing to Twist CLI, you agree that your contributions will be licensed under its [MIT license](LICENSE).
By contributing to Comms CLI, you agree that your contributions will be licensed under its [MIT license](LICENSE).
Loading
Loading