talking-stick 0.4.9 → 0.4.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A CLI coordination tool that lets multiple AI coding agents share a single workspace without stepping on each other. One agent holds the stick at a time; handoffs carry structured context so the next agent doesn't have to re-derive it.
4
4
 
5
- **Version:** 0.4.1. Multi-process-safe (SQLite WAL), liveness-aware, no daemon. Supports Claude Code, Codex CLI, Gemini CLI, and OpenCode out of the box. Two agents in the same room can also chat out-of-band — without passing the stick — via `tt msg send/recv`.
5
+ **Version:** 0.4.1. Multi-process-safe (SQLite WAL), liveness-aware, no daemon. Supports Claude Code, Codex CLI, Gemini CLI, Grok Build, and OpenCode out of the box. Two agents in the same room can also chat out-of-band — without passing the stick — via `tt msg send/recv`.
6
6
 
7
7
  ## Quickstart
8
8
 
@@ -166,13 +166,14 @@ For harnesses that only notice completed subprocesses, run `tt events --wait --a
166
166
  - Claude Code: copied or linked into `~/.claude/skills/talking-stick`
167
167
  - Codex: copied or linked into `~/.codex/skills/talking-stick`
168
168
  - Gemini: installed with `gemini skills install ... --scope user` or linked with `gemini skills link ... --scope user`
169
- - OpenCode: copied or linked into `~/.opencode/skills/talking-stick`
169
+ - Grok Build: copied or linked into `~/.grok/skills/talking-stick`, plus a trusted global session hook at `~/.grok/hooks/talking-stick-session.json`
170
+ - OpenCode: copied or linked into the resolved OpenCode config directory, normally `~/.config/opencode/skills/talking-stick` and honoring `XDG_CONFIG_HOME`
170
171
 
171
172
  By default, `tt install` links the bundled skill into each harness so local updates are picked up immediately. Pass `--copy` if you want a standalone snapshot instead.
172
173
 
173
- Stale MCP cleanup is strict for OpenCode JSON entries: it removes only the canonical `mcp.talking-stick` value with `["tt", "mcp"]` and leaves hand-edited entries alone. Claude Code, Codex, and Gemini cleanup uses their own `mcp remove` commands when the old server name exists. Every cleanup run appends JSONL audit entries to `${TALKING_STICK_DATA_DIR}/update-migrations.log`.
174
+ Stale MCP cleanup is strict for OpenCode JSON entries: it removes only the canonical `mcp.talking-stick` value with `["tt", "mcp"]` and leaves hand-edited entries alone. Claude Code, Codex, and Gemini cleanup uses their own `mcp remove` commands when the old server name exists. Grok Build has no Talking Stick MCP registration path; install is native skill plus hook only. Every cleanup run appends JSONL audit entries to `${TALKING_STICK_DATA_DIR}/update-migrations.log`.
174
175
 
175
- Human CLI invocations also perform a silent best-effort sync for already-installed file-based skills in Claude Code, Codex, and OpenCode. If the installed skill is a copy, it is refreshed from the bundled skill; if it is a stale symlink, it is relinked. Missing harness config directories and missing skill installs are skipped. Gemini skills are managed by Gemini's own registry, so use `tt install gemini` after updating when needed.
176
+ Human CLI invocations also perform a silent best-effort sync for already-installed file-based skills in Claude Code, Codex, Grok Build, and OpenCode. If the installed skill is a copy, it is refreshed from the bundled skill; if it is a stale symlink, it is relinked. Missing harness config directories and missing skill installs are skipped. Gemini skills are managed by Gemini's own registry, so use `tt install gemini` after updating when needed.
176
177
 
177
178
  ## Human CLI
178
179
 
@@ -189,7 +190,7 @@ tt state [path] # full room state
189
190
  tt events [path] [--after N] [--limit N] [--wait|--follow] [--event TYPE[,TYPE]] [--target self|any|agent] # room event log; --wait/--follow long-polls
190
191
  tt msg send <recipient|room> <body...> [--interrupt] [--stdin] [--path DIR] # send an OOB message
191
192
  tt msg recv [--wait|--follow] [--from agent] [--after N] [--target self|any|agent] [--path DIR] # receive OOB messages
192
- tt instructions show [path] [--harness claude|codex|gemini|opencode|all] [--scope effective|bundled|user|project] # show collaboration prompt
193
+ tt instructions show [path] [--harness claude|codex|gemini|grok|opencode|all] [--scope effective|bundled|user|project] # show collaboration prompt
193
194
  tt instructions edit [path] [--user|--project] # edit user or project prompt
194
195
  tt instructions reset [path] (--user|--project) # delete a user or project prompt
195
196
  tt release [path] --status TEXT --next-action TEXT # normal handoff
@@ -216,7 +217,8 @@ By default, `tt` behaves like a human CLI and resolves to `human:<username>` onl
216
217
 
217
218
  Harness-aware CLI identity is resolved before the human fallback:
218
219
 
219
- - Known harness environment markers such as `CLAUDECODE=1`, `CODEX_THREAD_ID`, `GEMINI_CLI=1`, or `OPENCODE=1` make `tt` derive a harness-style identity automatically.
220
+ - Known harness environment markers such as `CLAUDECODE=1`, `CODEX_THREAD_ID`, `GEMINI_CLI=1`, `CMUX_AGENT_LAUNCH_KIND=grok`, or `OPENCODE=1` make `tt` derive a harness-style identity automatically. The cmux Grok marker is optional; Grok Build also works without cmux by walking process ancestry for a `grok` root process.
221
+ - Grok Build's installed hook records hook-only `GROK_SESSION_ID` context into `${TALKING_STICK_DATA_DIR}/grok-sessions.jsonl`, letting later Grok-launched `tt` calls upgrade from process identity to the real Grok session id. `GROK_SESSION_ID` by itself is not treated as a normal shell marker, and the hook is not required for basic Grok detection.
220
222
  - Set `TT_HARNESS_AGENT_ID=<agent-id>` if the harness wants to export the exact agent id directly.
221
223
  - Set `TT_HARNESS_EXPORT=1` only when you need ancestry-based harness detection without a known harness environment marker.
222
224
 
@@ -0,0 +1,21 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ export function writeFileAtomic(filePath, data) {
5
+ const directory = path.dirname(filePath);
6
+ fs.mkdirSync(directory, { recursive: true });
7
+ const tempPath = path.join(directory, `.${path.basename(filePath)}.${process.pid}.${Date.now()}.${crypto.randomUUID()}.tmp`);
8
+ try {
9
+ fs.writeFileSync(tempPath, data);
10
+ fs.renameSync(tempPath, filePath);
11
+ }
12
+ catch (error) {
13
+ try {
14
+ fs.rmSync(tempPath, { force: true });
15
+ }
16
+ catch {
17
+ // Best effort cleanup for failed writes.
18
+ }
19
+ throw error;
20
+ }
21
+ }
@@ -0,0 +1,69 @@
1
+ import { createSystemProcessInspector } from "../process-utils.js";
2
+ import { appendGrokSessionRecord } from "../grok-session-store.js";
3
+ import { findHarnessRootInAncestry } from "../identity.js";
4
+ export async function runGrokSessionHookCommand(options = {}) {
5
+ try {
6
+ const env = options.env ?? process.env;
7
+ const input = parseHookInput(options.stdin ?? await readStdin());
8
+ const sessionId = firstNonEmptyString(env.GROK_SESSION_ID, input.sessionId);
9
+ const workspaceRoot = firstNonEmptyString(env.GROK_WORKSPACE_ROOT, env.CLAUDE_PROJECT_DIR, input.workspaceRoot);
10
+ if (!sessionId || !workspaceRoot) {
11
+ return;
12
+ }
13
+ const event = firstNonEmptyString(env.GROK_HOOK_EVENT, input.hookEventName) ?? "unknown";
14
+ const inspector = options.inspector ?? createSystemProcessInspector();
15
+ const parentPid = options.parentPid ?? process.ppid;
16
+ const parentInspection = inspector.inspect(parentPid);
17
+ const grokRoot = findHarnessRootInAncestry("grok", parentPid, parentInspection, inspector, 20);
18
+ const record = {
19
+ source: "grok_hook",
20
+ grok_session_id: sessionId,
21
+ workspace_root: workspaceRoot,
22
+ cwd: firstNonEmptyString(input.cwd),
23
+ event,
24
+ observed_at: firstNonEmptyString(input.timestamp) ??
25
+ (options.now ?? new Date()).toISOString(),
26
+ grok_pid: grokRoot?.pid ?? null,
27
+ grok_process_started_at: grokRoot?.startTime ?? null
28
+ };
29
+ appendGrokSessionRecord(record, { logPath: options.logPath });
30
+ }
31
+ catch {
32
+ // Grok hooks must fail open. Identity can fall back to pid-root detection
33
+ // when the hook cannot record a session row.
34
+ }
35
+ }
36
+ function parseHookInput(raw) {
37
+ const trimmed = raw.trim();
38
+ if (!trimmed)
39
+ return {};
40
+ try {
41
+ const parsed = JSON.parse(trimmed);
42
+ return isObjectRecord(parsed) ? parsed : {};
43
+ }
44
+ catch {
45
+ return {};
46
+ }
47
+ }
48
+ function readStdin() {
49
+ return new Promise((resolve, reject) => {
50
+ let raw = "";
51
+ process.stdin.setEncoding("utf8");
52
+ process.stdin.on("data", (chunk) => {
53
+ raw += chunk;
54
+ });
55
+ process.stdin.on("error", reject);
56
+ process.stdin.on("end", () => resolve(raw));
57
+ });
58
+ }
59
+ function firstNonEmptyString(...values) {
60
+ for (const value of values) {
61
+ if (typeof value === "string" && value.trim().length > 0) {
62
+ return value.trim();
63
+ }
64
+ }
65
+ return null;
66
+ }
67
+ function isObjectRecord(value) {
68
+ return typeof value === "object" && value !== null && !Array.isArray(value);
69
+ }
@@ -103,8 +103,46 @@ export async function spawnGuardian(input) {
103
103
  const inspector = createSystemProcessInspector();
104
104
  let stdout = "";
105
105
  let stderr = "";
106
+ let settled = false;
107
+ const cleanup = () => {
108
+ clearTimeout(timeout);
109
+ child.stdout?.removeAllListeners();
110
+ child.stderr?.removeAllListeners();
111
+ child.removeAllListeners("exit");
112
+ child.removeAllListeners("error");
113
+ child.stdout?.destroy();
114
+ child.stderr?.destroy();
115
+ };
116
+ const killChild = () => {
117
+ try {
118
+ child.kill("SIGTERM");
119
+ }
120
+ catch {
121
+ // Best effort cleanup for a child that failed readiness.
122
+ }
123
+ };
124
+ const rejectOnce = (error, kill = false) => {
125
+ if (settled) {
126
+ return;
127
+ }
128
+ settled = true;
129
+ if (kill) {
130
+ killChild();
131
+ }
132
+ cleanup();
133
+ reject(error);
134
+ };
135
+ const resolveOnce = (value) => {
136
+ if (settled) {
137
+ return;
138
+ }
139
+ settled = true;
140
+ cleanup();
141
+ child.unref();
142
+ resolve(value);
143
+ };
106
144
  const timeout = setTimeout(() => {
107
- reject(new Error("Guardian did not signal readiness in time."));
145
+ rejectOnce(new Error("Guardian did not signal readiness in time."), true);
108
146
  }, GUARD_READY_TIMEOUT_MS);
109
147
  child.stdout?.setEncoding("utf8");
110
148
  child.stderr?.setEncoding("utf8");
@@ -113,15 +151,11 @@ export async function spawnGuardian(input) {
113
151
  if (!stdout.includes(GUARD_READY)) {
114
152
  return;
115
153
  }
116
- clearTimeout(timeout);
117
- child.stdout?.destroy();
118
- child.stderr?.destroy();
119
- child.unref();
120
154
  if (!child.pid) {
121
- reject(new Error("Guardian started without a PID."));
155
+ rejectOnce(new Error("Guardian started without a PID."), true);
122
156
  return;
123
157
  }
124
- resolve({
158
+ resolveOnce({
125
159
  pid: child.pid,
126
160
  process_started_at: inspector.inspect(child.pid)?.startTime ?? null
127
161
  });
@@ -130,8 +164,10 @@ export async function spawnGuardian(input) {
130
164
  stderr += chunk;
131
165
  });
132
166
  child.on("exit", (code) => {
133
- clearTimeout(timeout);
134
- reject(new Error(`Guardian exited before readiness (code ${code ?? "unknown"}): ${stderr.trim()}`));
167
+ rejectOnce(new Error(`Guardian exited before readiness (code ${code ?? "unknown"}): ${stderr.trim()}`));
168
+ });
169
+ child.on("error", (error) => {
170
+ rejectOnce(error instanceof Error ? error : new Error(String(error)));
135
171
  });
136
172
  });
137
173
  }
@@ -1,16 +1,13 @@
1
1
  import { spawn } from "node:child_process";
2
- import { SUPPORTED_HARNESSES, detectHarness, parseHarnessList, planUninstall, runAction } from "../install.js";
2
+ import { SUPPORTED_HARNESSES, detectHarness, parseHarnessList, planGrokSessionHookInstall, planGrokSessionHookUninstall, planUninstall, runAction } from "../install.js";
3
3
  import { planSkillInstall, planSkillUninstall } from "../skill-install.js";
4
4
  import { resolveDataDir } from "../config.js";
5
5
  import { FileAuditLog, defaultAuditLogPath } from "../install-audit.js";
6
6
  import { removeStaleMcpRegistrations } from "../install-migration.js";
7
7
  import { detectInstallSource, isPackageManager, planSelfUpdate, resolveCurrentBinaryPath } from "../self-update.js";
8
8
  import { readPackageVersion, runStaleMcpCleanup } from "../update-migration.js";
9
- import { getStringOption, hasOption, normalizeBooleanFlag } from "./parser.js";
9
+ import { getStringOption, hasOption } from "./parser.js";
10
10
  export async function runInstallCommand(parsed) {
11
- normalizeBooleanFlag(parsed, "print");
12
- normalizeBooleanFlag(parsed, "copy");
13
- normalizeBooleanFlag(parsed, "link");
14
11
  const harnesses = selectHarnesses(parsed);
15
12
  const dryRun = hasOption(parsed, "print");
16
13
  const installOptions = {
@@ -32,7 +29,6 @@ export async function runInstallCommand(parsed) {
32
29
  printInstructionHint(results);
33
30
  }
34
31
  export async function runUninstallCommand(parsed) {
35
- normalizeBooleanFlag(parsed, "print");
36
32
  const harnesses = selectHarnesses(parsed);
37
33
  const dryRun = hasOption(parsed, "print");
38
34
  const installOptions = { skipMissing: true };
@@ -48,9 +44,6 @@ export async function runUninstallCommand(parsed) {
48
44
  reportCleanupResults(await runCleanup(harnesses, "uninstall", installOptions), "uninstall");
49
45
  }
50
46
  export async function runInstallSkillCommand(parsed) {
51
- normalizeBooleanFlag(parsed, "print");
52
- normalizeBooleanFlag(parsed, "copy");
53
- normalizeBooleanFlag(parsed, "link");
54
47
  const harnesses = selectHarnesses(parsed);
55
48
  const dryRun = hasOption(parsed, "print");
56
49
  const link = resolveSkillInstallLinkMode(parsed);
@@ -66,7 +59,6 @@ export async function runInstallSkillCommand(parsed) {
66
59
  reportInstallResults(results, "install");
67
60
  }
68
61
  export async function runUninstallSkillCommand(parsed) {
69
- normalizeBooleanFlag(parsed, "print");
70
62
  const harnesses = selectHarnesses(parsed);
71
63
  const dryRun = hasOption(parsed, "print");
72
64
  const installOptions = { skipMissing: true };
@@ -81,7 +73,6 @@ export async function runUninstallSkillCommand(parsed) {
81
73
  reportInstallResults(results, "uninstall");
82
74
  }
83
75
  export async function runSelfUpdateCommand(parsed, cliEntryUrl) {
84
- normalizeBooleanFlag(parsed, "print");
85
76
  const dryRun = hasOption(parsed, "print");
86
77
  const managerOverride = getStringOption(parsed, "manager");
87
78
  let source;
@@ -121,7 +112,6 @@ export async function runSelfUpdateCommand(parsed, cliEntryUrl) {
121
112
  process.stdout.write("Done. Restart any long-running harness sessions to pick up the new tt.\n");
122
113
  }
123
114
  export async function runMcpMigrationCommand(parsed) {
124
- normalizeBooleanFlag(parsed, "quiet");
125
115
  const reason = parseAuditReason(getStringOption(parsed, "reason") ?? "manual");
126
116
  const quiet = hasOption(parsed, "quiet");
127
117
  const cleanup = await runStaleMcpCleanup({
@@ -145,7 +135,7 @@ function resolveSkillInstallLinkMode(parsed) {
145
135
  return true;
146
136
  }
147
137
  function planInstallActions(harnesses, installOptions) {
148
- return harnesses.map((harness) => planSkillInstall(harness, installOptions));
138
+ return harnesses.flatMap((harness) => planInstallActionsForHarness(harness, installOptions));
149
139
  }
150
140
  function planUninstallActions(harnesses, installOptions) {
151
141
  return harnesses.flatMap((harness) => [
@@ -153,6 +143,14 @@ function planUninstallActions(harnesses, installOptions) {
153
143
  ...installOptions,
154
144
  skipMissing: false
155
145
  }),
146
+ ...(harness === "grok"
147
+ ? [
148
+ planGrokSessionHookUninstall({
149
+ ...installOptions,
150
+ skipMissing: false
151
+ })
152
+ ]
153
+ : []),
156
154
  planUninstall(harness, installOptions)
157
155
  ]);
158
156
  }
@@ -160,14 +158,31 @@ function planCleanupActions(harnesses, installOptions) {
160
158
  return harnesses.map((harness) => planUninstall(harness, installOptions));
161
159
  }
162
160
  async function runSkillInstall(harness, installOptions) {
163
- const skillAction = planSkillInstall(harness, installOptions);
164
- const skillResult = await runAction(skillAction, installOptions);
165
- return [skillResult];
161
+ const actions = planInstallActionsForHarness(harness, installOptions);
162
+ return Promise.all(actions.map((action) => runAction(action, installOptions)));
166
163
  }
167
164
  async function runSkillUninstall(harness, installOptions) {
168
- const skillAction = planSkillUninstall(harness, installOptions);
169
- const skillResult = await runAction(skillAction, installOptions);
170
- return [skillResult];
165
+ const actions = [
166
+ planSkillUninstall(harness, {
167
+ ...installOptions,
168
+ skipMissing: false
169
+ }),
170
+ ...(harness === "grok"
171
+ ? [
172
+ planGrokSessionHookUninstall({
173
+ ...installOptions,
174
+ skipMissing: false
175
+ })
176
+ ]
177
+ : [])
178
+ ];
179
+ return Promise.all(actions.map((action) => runAction(action, installOptions)));
180
+ }
181
+ function planInstallActionsForHarness(harness, installOptions) {
182
+ return [
183
+ planSkillInstall(harness, installOptions),
184
+ ...(harness === "grok" ? [planGrokSessionHookInstall(installOptions)] : [])
185
+ ];
171
186
  }
172
187
  async function runCleanup(harnesses, reason, installOptions) {
173
188
  const dataDir = resolveDataDir();
@@ -24,8 +24,6 @@ export async function handleInstructionsCommand(parsed) {
24
24
  }
25
25
  }
26
26
  function handleInstructionsShowCommand(parsed) {
27
- repairBooleanFlag(parsed, "json", 0);
28
- repairBooleanFlag(parsed, "text", 0);
29
27
  const contextPath = resolveContextPathArg(parsed);
30
28
  const scope = parseInstructionScope(getStringOption(parsed, "scope"));
31
29
  const identity = deriveCliIdentity(parsed);
@@ -45,10 +43,6 @@ function handleInstructionsShowCommand(parsed) {
45
43
  });
46
44
  }
47
45
  async function handleInstructionsEditCommand(parsed) {
48
- repairBooleanFlag(parsed, "json", 0);
49
- repairBooleanFlag(parsed, "text", 0);
50
- repairBooleanFlag(parsed, "user", 0);
51
- repairBooleanFlag(parsed, "project", 0);
52
46
  const contextPath = resolveContextPathArg(parsed);
53
47
  const scope = resolveEditableScope(parsed, false);
54
48
  const result = await editInstructions({
@@ -66,10 +60,6 @@ async function handleInstructionsEditCommand(parsed) {
66
60
  });
67
61
  }
68
62
  function handleInstructionsResetCommand(parsed) {
69
- repairBooleanFlag(parsed, "json", 0);
70
- repairBooleanFlag(parsed, "text", 0);
71
- repairBooleanFlag(parsed, "user", 0);
72
- repairBooleanFlag(parsed, "project", 0);
73
63
  const contextPath = resolveContextPathArg(parsed);
74
64
  const scope = resolveEditableScope(parsed, true);
75
65
  const result = resetInstructions({
@@ -104,10 +94,3 @@ function resolveContextPathArg(parsed) {
104
94
  }
105
95
  return pathOption ?? parsed.positionals[0] ?? process.cwd();
106
96
  }
107
- function repairBooleanFlag(parsed, key, insertAt) {
108
- const value = parsed.options.get(key);
109
- if (typeof value === "string") {
110
- parsed.positionals.splice(insertAt, 0, value);
111
- parsed.options.set(key, true);
112
- }
113
- }
@@ -29,8 +29,6 @@ async function handleMsgSendCommand(runtime, parsed) {
29
29
  const identity = deriveCliIdentity(parsed);
30
30
  const session = resolveSessionForNotes(runtime, parsed, identity);
31
31
  const usesRoomFlag = hasOption(parsed, "room");
32
- repairBooleanFlag(parsed, "room", 0);
33
- repairBooleanFlag(parsed, "interrupt", usesRoomFlag ? 0 : 1);
34
32
  const recipientSelector = usesRoomFlag ? "room" : parsed.positionals[0];
35
33
  if (!recipientSelector) {
36
34
  throw new Error("Usage: tt msg send <recipient|room> <body...> [--interrupt] [--stdin].");
@@ -69,13 +67,6 @@ async function handleMsgRecvCommand(runtime, parsed) {
69
67
  force_tail_cursor: false
70
68
  });
71
69
  }
72
- function repairBooleanFlag(parsed, key, insertAt) {
73
- const value = parsed.options.get(key);
74
- if (typeof value === "string") {
75
- parsed.positionals.splice(insertAt, 0, value);
76
- parsed.options.set(key, true);
77
- }
78
- }
79
70
  function shortEventId(eventId) {
80
71
  return eventId.slice(0, 8);
81
72
  }
@@ -164,7 +164,7 @@ Commands:
164
164
  tt events [path] [--after N] [--limit N] [--wait|--follow] [--event TYPE[,TYPE]] [--target self|any|agent]
165
165
  tt msg send <recipient|room> <body...> [--interrupt] [--stdin] [--path DIR]
166
166
  tt msg recv [--wait|--follow] [--from agent] [--after N] [--target self|any|agent] [--path DIR]
167
- tt instructions show [path] [--harness claude|codex|gemini|opencode|all] [--scope effective|bundled|user|project]
167
+ tt instructions show [path] [--harness claude|codex|gemini|grok|opencode|all] [--scope effective|bundled|user|project]
168
168
  tt instructions edit [path] [--user|--project]
169
169
  tt instructions reset [path] (--user|--project)
170
170
  tt release [path] (--status TEXT --next-action TEXT | --stdin)
@@ -1,3 +1,26 @@
1
+ const BOOLEAN_FLAGS = new Set([
2
+ "all",
3
+ "copy",
4
+ "events",
5
+ "explain",
6
+ "follow",
7
+ "force",
8
+ "force-new",
9
+ "help",
10
+ "interrupt",
11
+ "json",
12
+ "link",
13
+ "operator-requested",
14
+ "park",
15
+ "print",
16
+ "project",
17
+ "quiet",
18
+ "room",
19
+ "stdin",
20
+ "text",
21
+ "user",
22
+ "wait"
23
+ ]);
1
24
  export function parseCommand(argv) {
2
25
  const [name = "", ...rest] = argv;
3
26
  const options = new Map();
@@ -9,6 +32,10 @@ export function parseCommand(argv) {
9
32
  continue;
10
33
  }
11
34
  const key = token.slice(2);
35
+ if (BOOLEAN_FLAGS.has(key)) {
36
+ options.set(key, true);
37
+ continue;
38
+ }
12
39
  const next = rest[index + 1];
13
40
  if (!next || next.startsWith("--")) {
14
41
  options.set(key, true);
@@ -33,18 +60,14 @@ export function requireStringOption(parsed, key) {
33
60
  }
34
61
  return value;
35
62
  }
36
- export function normalizeBooleanFlag(parsed, key) {
37
- const value = parsed.options.get(key);
38
- if (typeof value === "string") {
39
- parsed.positionals.unshift(value);
40
- parsed.options.set(key, true);
41
- }
42
- }
43
63
  export function parseOptionalInteger(parsed, key) {
44
64
  const value = getStringOption(parsed, key);
45
65
  if (!value) {
46
66
  return undefined;
47
67
  }
68
+ if (!/^\d+$/.test(value)) {
69
+ throw new Error(`--${key} must be an integer.`);
70
+ }
48
71
  const parsedValue = Number.parseInt(value, 10);
49
72
  if (!Number.isInteger(parsedValue)) {
50
73
  throw new Error(`--${key} must be an integer.`);
@@ -1,4 +1,5 @@
1
1
  import { runGuardCommand } from "./guardian.js";
2
+ import { runGrokSessionHookCommand } from "./grok-session-hook.js";
2
3
  import { runInstallCommand, runMcpMigrationCommand, runSelfUpdateCommand, runUninstallCommand } from "./install-commands.js";
3
4
  import { handleInstructionsCommand } from "./instructions-commands.js";
4
5
  import { handleMsgCommand } from "./msg-commands.js";
@@ -15,6 +16,15 @@ export const COMMAND_REGISTRY = [
15
16
  description: "Run an internal lease heartbeat guardian.",
16
17
  handler: ({ parsed }) => runGuardCommand(parsed)
17
18
  },
19
+ {
20
+ name: "grok-session-hook",
21
+ needsRuntime: false,
22
+ startupMaintenance: false,
23
+ internal: true,
24
+ usage: "tt grok-session-hook",
25
+ description: "Record Grok hook session context for identity resolution.",
26
+ handler: () => runGrokSessionHookCommand()
27
+ },
18
28
  {
19
29
  name: "install",
20
30
  needsRuntime: false,
@@ -1,7 +1,7 @@
1
1
  import { deriveCliIdentity, resolveCliIdentity } from "./identity.js";
2
2
  import { removeCliSession, removeCliSessionsForRoom, resolveCliSessionPath } from "../index.js";
3
3
  import { stopGuardian } from "./guardian.js";
4
- import { getStringOption, hasOption, normalizeBooleanFlag, parseOptionalInteger } from "./parser.js";
4
+ import { getStringOption, hasOption, parseOptionalInteger } from "./parser.js";
5
5
  import { formatRelativeTime, printResult } from "./output.js";
6
6
  import { parseEventTypeFilter, runEventStream } from "./event-stream.js";
7
7
  import { resolveSessionForReads, upsertSessionFromJoin } from "./session.js";
@@ -129,8 +129,6 @@ export function handleStateCommand(runtime, parsed) {
129
129
  });
130
130
  }
131
131
  export async function handleEventsCommand(runtime, parsed) {
132
- normalizeBooleanFlag(parsed, "wait");
133
- normalizeBooleanFlag(parsed, "follow");
134
132
  const identity = deriveCliIdentity(parsed);
135
133
  const session = resolveSessionForReads(runtime, parsed, identity);
136
134
  if (hasOption(parsed, "wait") || hasOption(parsed, "follow")) {
@@ -2,13 +2,11 @@ import { clearCliSessionLease, createSystemProcessInspector, findCliSessionByRoo
2
2
  import { checkGuardianLiveness, spawnGuardian, stopGuardian } from "./guardian.js";
3
3
  import { resolveHandoff } from "./handoff.js";
4
4
  import { deriveCliIdentity, resolveTakeoverReason, shouldUseOperatorOverride } from "./identity.js";
5
- import { getStringOption, hasOption, normalizeBooleanFlag, parseRequiredInteger, parseWaitTimeout } from "./parser.js";
5
+ import { getStringOption, hasOption, parseRequiredInteger, parseWaitTimeout } from "./parser.js";
6
6
  import { resolveTargetFilter } from "./event-stream.js";
7
7
  import { formatWaitResult, printResult } from "./output.js";
8
8
  import { requireLeaseSession, upsertSessionFromJoin } from "./session.js";
9
9
  export async function handleWaitCommand(runtime, parsed, isTry, cliEntryUrl) {
10
- normalizeBooleanFlag(parsed, "park");
11
- normalizeBooleanFlag(parsed, "events");
12
10
  const park = hasOption(parsed, "park");
13
11
  const includeEvents = hasOption(parsed, "events");
14
12
  const afterEventSeq = includeEvents
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { isProtocolError } from "./index.js";
6
6
  import { parseCommand } from "./cli/parser.js";
7
- import { printHelp } from "./cli/output.js";
7
+ import { printHelp, shouldUseJson } from "./cli/output.js";
8
8
  import { getCommand } from "./cli/registry.js";
9
9
  import { createRuntime } from "./cli/runtime.js";
10
10
  import { runStartupMaintenance } from "./cli/startup-maintenance.js";
@@ -49,12 +49,24 @@ function isDirectExecution() {
49
49
  }
50
50
  if (isDirectExecution()) {
51
51
  await runCli().catch((error) => {
52
- const message = isProtocolError(error)
53
- ? JSON.stringify(error.toJSON(), null, 2)
54
- : error instanceof Error
52
+ const parsed = parseCommand(process.argv.slice(2));
53
+ if (shouldUseJson(parsed)) {
54
+ const payload = isProtocolError(error)
55
+ ? error.toJSON()
56
+ : {
57
+ error: "cli_error",
58
+ message: error instanceof Error ? error.message : String(error)
59
+ };
60
+ process.stderr.write(`${JSON.stringify(payload, null, 2)}\n`);
61
+ }
62
+ else {
63
+ const message = isProtocolError(error)
55
64
  ? error.message
56
- : String(error);
57
- process.stderr.write(`${message}\n`);
65
+ : error instanceof Error
66
+ ? error.message
67
+ : String(error);
68
+ process.stderr.write(`${message}\n`);
69
+ }
58
70
  process.exit(1);
59
71
  });
60
72
  }