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 +8 -6
- package/dist/atomic-write.js +21 -0
- package/dist/cli/grok-session-hook.js +69 -0
- package/dist/cli/guardian.js +45 -9
- package/dist/cli/install-commands.js +34 -19
- package/dist/cli/instructions-commands.js +0 -17
- package/dist/cli/msg-commands.js +0 -9
- package/dist/cli/output.js +1 -1
- package/dist/cli/parser.js +30 -7
- package/dist/cli/registry.js +10 -0
- package/dist/cli/room-commands.js +1 -3
- package/dist/cli/turn-commands.js +1 -3
- package/dist/cli.js +18 -6
- package/dist/grok-session-store.js +143 -0
- package/dist/identity.js +77 -8
- package/dist/index.js +2 -1
- package/dist/install.js +127 -15
- package/dist/instructions.js +19 -4
- package/dist/process-utils.js +5 -1
- package/dist/service.js +20 -11
- package/dist/session-store.js +2 -2
- package/dist/skill-install.js +35 -4
- package/docs/releases/0.4.10.md +17 -0
- package/docs/releases/0.4.11.md +28 -0
- package/package.json +1 -1
- package/skills/talking-stick/SKILL.md +2 -2
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
|
-
-
|
|
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
|
+
}
|
package/dist/cli/guardian.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
155
|
+
rejectOnce(new Error("Guardian started without a PID."), true);
|
|
122
156
|
return;
|
|
123
157
|
}
|
|
124
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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
|
|
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.
|
|
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
|
|
164
|
-
|
|
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
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
}
|
package/dist/cli/msg-commands.js
CHANGED
|
@@ -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
|
}
|
package/dist/cli/output.js
CHANGED
|
@@ -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)
|
package/dist/cli/parser.js
CHANGED
|
@@ -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.`);
|
package/dist/cli/registry.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
:
|
|
57
|
-
|
|
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
|
}
|