add-command-71a65d — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited add-command-71a65d (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Follow this checklist when adding new commands. Each step references the exact file to modify.
src/lib/api.ts)Add an entry to API_SPINNER_MESSAGES for each new SDK method.
Color convention:
blue — read/fetch operations (e.g., loading threads, listing channels)green — create/join operations (e.g., creating a thread, starting a conversation)yellow — update/delete/archive mutations (e.g., muting, deleting, archiving)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).
KNOWN_SAFE_API_METHODSsrc/commands/<entity>/)Commands with multiple subcommands use a folder-based structure:
src/commands/<entity>/
index.ts # registerXxxCommand — creates parent cmd, wires subcommands
list.ts # async function listXxx(...) — one file per subcommand
view.ts # async function viewXxx(...)
create.ts # async function createXxx(...)
helpers.ts # shared constants/utilities used by multiple subcommands (optional)registerXxxCommand../../lib/ for lib imports. No Commander imports (only index.ts uses Commander).Single-subcommand commands (e.g., channel.ts, inbox.ts) remain as flat files.
src/commands/<entity>/<action>.ts with the handler functionsrc/commands/<entity>/index.ts| Command type | Flags |
|---|---|
| Read-only | --json (and --ndjson for lists) |
| Mutating (returns entity) | --json (use formatJson), --dry-run |
| Mutating (no return) | --dry-run |
| Destructive + irreversible | --yes, --dry-run |
| Reversible (archive/unarchive) | --dry-run (no --yes) |
resolveThreadId(ref) — resolve thread by numeric ID or Twist URLresolveChannelId(ref) — resolve channel by numeric ID, URL, or fuzzy nameresolveWorkspaceRef(ref) — resolve workspace by ID or fuzzy nameresolveConversationId(ref) — resolve conversation by numeric ID or URLparseRef(ref) — low-level parser: returns { type: 'id' | 'url' | 'name', ... }Add new resolver wrappers in refs.ts when needed.
const myCmd = parent
.command('my-action [ref]')
.description('Do something')
.option('--json', 'Output as JSON')
.option('--dry-run', 'Preview what would happen without executing')
.action((ref, options) => {
if (!ref) {
myCmd.help()
return
}
return myAction(ref, options)
})The variable assignment (const myCmd = ...) is needed so the .action() callback can call myCmd.help() when the argument is missing.
For entity commands with a view subcommand, mark it as the default so tw thread 123 maps to tw thread view 123:
thread
.command('view [thread-ref]', { isDefault: true })
.description('Display a thread with its comments')
.action((ref, options) => viewThread(ref, options))Where commands accept positional [workspace-ref], also accept a --workspace flag. Error if both are provided:
if (workspaceRef && options.workspace) {
throw new Error('Cannot specify workspace both as argument and --workspace flag')
}
const ref = workspaceRef ?? options.workspaceNever use `process.exit(1)` in command handlers. It terminates immediately without running finally blocks, leaving the spinner stuck. Use process.exitCode = 1 followed by return instead.
New top-level commands must be registered in src/index.ts using the lazy loading pattern:
const loadMyCommand = async () => (await import('./commands/my-entity/index.js')).registerMyCommand
const commands: Record<string, [string, () => Promise<(p: Command) => void>]> = {
// ... existing commands
'my-entity': ['My entity operations', loadMyCommand],
}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.
archived, muted are self-explanatory.chalk.dim() for secondary info is fine — screen readers ignore styling.import { isAccessible } from '../lib/output.js'
const a11y = isAccessible()
const prefix = a11y ? '[!] ' : ''
console.log(chalk.yellow(`${prefix}Warning: thread is archived`))src/__tests__/<entity>.test.ts)Tests mock the API layer directly using vi.mock and vi.hoisted. Follow the existing pattern in test files like thread.test.ts or conversation.test.ts.
const apiMocks = vi.hoisted(() => ({
getTwistClient: vi.fn(),
}))
vi.mock('../lib/api.js', async (importOriginal) => ({
...(await importOriginal<typeof import('../lib/api.js')>()),
getTwistClient: apiMocks.getTwistClient,
}))
vi.mock('../lib/markdown.js', () => ({
renderMarkdown: vi.fn((text: string) => text),
}))
vi.mock('chalk')Build a mock client object that matches the SDK structure:
function createClient({ thread, comments, channel } = {}) {
return {
threads: {
getThread: vi.fn().mockResolvedValue(thread),
createThread: vi.fn().mockResolvedValue(thread),
},
channels: {
getChannel: vi.fn().mockResolvedValue(channel),
},
// ... add mock methods as needed
}
}--dry-run for mutating commands (API method should NOT be called, preview text shown)--json output where applicablesrc/lib/skills/content.ts)Update SKILL_CONTENT with examples for the new command. Update relevant sections:
### Section block--json list if the command returns an entity--dry-run list if applicableAfter all code changes are complete:
npm run build && npm run sync:skillThis 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.
npm run type-check
npm test
npm run lint:check~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.