talking-stick 0.1.4 → 0.3.0

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.
@@ -1,19 +1,10 @@
1
- import { runStdioServer } from "../index.js";
2
1
  import { runGuardCommand } from "./guardian.js";
3
- import { runInstallCommand, runInstallSkillCommand, runSelfUpdateCommand, runUninstallCommand, runUninstallSkillCommand } from "./install-commands.js";
2
+ import { runInstallCommand, runMcpMigrationCommand, runSelfUpdateCommand, runUninstallCommand } from "./install-commands.js";
3
+ import { handleMsgCommand } from "./msg-commands.js";
4
4
  import { handleNotesCommand } from "./notes-commands.js";
5
5
  import { handleEventsCommand, handleJoinCommand, handleKickCommand, handleLeaveCommand, handleListCommand, handleStateCommand, handleWhoAmICommand } from "./room-commands.js";
6
6
  import { handleAssignCommand, handlePassCommand, handleReleaseCommand, handleTakeCommand, handleWaitCommand } from "./turn-commands.js";
7
7
  export const COMMAND_REGISTRY = [
8
- {
9
- name: "mcp",
10
- needsRuntime: false,
11
- startupMaintenance: false,
12
- internal: true,
13
- usage: "tt mcp",
14
- description: "Run the MCP server over stdio.",
15
- handler: () => runStdioServer()
16
- },
17
8
  {
18
9
  name: "guard",
19
10
  needsRuntime: false,
@@ -29,7 +20,7 @@ export const COMMAND_REGISTRY = [
29
20
  startupMaintenance: false,
30
21
  internal: false,
31
22
  usage: "tt install <harness...> | --all [--print] [--copy] [--link]",
32
- description: "Install Talking Stick into harness MCP configs and skills.",
23
+ description: "Install the Talking Stick skill and remove stale MCP registrations.",
33
24
  handler: ({ parsed }) => runInstallCommand(parsed)
34
25
  },
35
26
  {
@@ -38,35 +29,26 @@ export const COMMAND_REGISTRY = [
38
29
  startupMaintenance: false,
39
30
  internal: false,
40
31
  usage: "tt uninstall <harness...> | --all [--print]",
41
- description: "Remove Talking Stick from harness MCP configs and skills.",
32
+ description: "Remove the Talking Stick skill and stale MCP registrations.",
42
33
  handler: ({ parsed }) => runUninstallCommand(parsed)
43
34
  },
44
35
  {
45
- name: "install-skill",
36
+ name: "self-update",
46
37
  needsRuntime: false,
47
38
  startupMaintenance: false,
48
39
  internal: false,
49
- usage: "tt install-skill <harness...> | --all [--print] [--copy] [--link]",
50
- description: "Install the bundled Talking Stick skill.",
51
- handler: ({ parsed }) => runInstallSkillCommand(parsed)
40
+ usage: "tt self-update [--print] [--manager npm|pnpm|yarn|bun]",
41
+ description: "Update the globally installed tt package.",
42
+ handler: ({ parsed, cliEntryUrl }) => runSelfUpdateCommand(parsed, cliEntryUrl)
52
43
  },
53
44
  {
54
- name: "uninstall-skill",
45
+ name: "migrate-mcp",
55
46
  needsRuntime: false,
56
47
  startupMaintenance: false,
57
- internal: false,
58
- usage: "tt uninstall-skill <harness...> | --all [--print]",
59
- description: "Remove the bundled Talking Stick skill.",
60
- handler: ({ parsed }) => runUninstallSkillCommand(parsed)
61
- },
62
- {
63
- name: "self-update",
64
- needsRuntime: false,
65
- startupMaintenance: true,
66
- internal: false,
67
- usage: "tt self-update [--print] [--manager npm|pnpm|yarn|bun]",
68
- description: "Update the globally installed tt package.",
69
- handler: ({ parsed, cliEntryUrl }) => runSelfUpdateCommand(parsed, cliEntryUrl)
48
+ internal: true,
49
+ usage: "tt migrate-mcp [--reason update|first-run|uninstall|manual] [--quiet]",
50
+ description: "Remove stale Talking Stick MCP registrations.",
51
+ handler: ({ parsed }) => runMcpMigrationCommand(parsed)
70
52
  },
71
53
  {
72
54
  name: "whoami",
@@ -127,16 +109,25 @@ export const COMMAND_REGISTRY = [
127
109
  needsRuntime: true,
128
110
  startupMaintenance: true,
129
111
  internal: false,
130
- usage: "tt events [path] [--after N] [--limit N]",
112
+ usage: "tt events [path] [--after N] [--limit N] [--wait|--follow] [--event TYPE[,TYPE]] [--target self|any|agent]",
131
113
  description: "Show room events.",
132
114
  handler: ({ runtime, parsed }) => handleEventsCommand(requireRuntime(runtime), parsed)
133
115
  },
116
+ {
117
+ name: "msg",
118
+ needsRuntime: true,
119
+ startupMaintenance: true,
120
+ internal: false,
121
+ usage: "tt msg <send|recv> [...]",
122
+ description: "Send or receive transient messages on a room's event stream.",
123
+ handler: ({ runtime, parsed }) => handleMsgCommand(requireRuntime(runtime), parsed)
124
+ },
134
125
  {
135
126
  name: "wait",
136
127
  needsRuntime: true,
137
128
  startupMaintenance: true,
138
129
  internal: false,
139
- usage: "tt wait [path] [--timeout 30s]",
130
+ usage: "tt wait [path] [--timeout 110s]",
140
131
  description: "Wait until this agent can claim the stick.",
141
132
  handler: ({ runtime, parsed, cliEntryUrl }) => handleWaitCommand(requireRuntime(runtime), parsed, false, cliEntryUrl)
142
133
  },
@@ -1,8 +1,9 @@
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, parseOptionalInteger } from "./parser.js";
4
+ import { getStringOption, hasOption, normalizeBooleanFlag, parseOptionalInteger } from "./parser.js";
5
5
  import { formatRelativeTime, printResult } from "./output.js";
6
+ import { parseEventTypeFilter, runEventStream } from "./event-stream.js";
6
7
  import { resolveSessionForReads, upsertSessionFromJoin } from "./session.js";
7
8
  export function handleListCommand(runtime, parsed) {
8
9
  const contextPath = parsed.positionals[0] ?? process.cwd();
@@ -126,9 +127,19 @@ export function handleStateCommand(runtime, parsed) {
126
127
  return lines.join("\n");
127
128
  });
128
129
  }
129
- export function handleEventsCommand(runtime, parsed) {
130
+ export async function handleEventsCommand(runtime, parsed) {
131
+ normalizeBooleanFlag(parsed, "wait");
132
+ normalizeBooleanFlag(parsed, "follow");
130
133
  const identity = deriveCliIdentity(parsed);
131
134
  const session = resolveSessionForReads(runtime, parsed, identity);
135
+ if (hasOption(parsed, "wait") || hasOption(parsed, "follow")) {
136
+ await runEventStream(runtime, parsed, identity, session.room_id, {
137
+ event_type: parseEventTypeFilter(getStringOption(parsed, "event")),
138
+ default_target: "self",
139
+ force_tail_cursor: false
140
+ });
141
+ return;
142
+ }
132
143
  const events = runtime.commands.getRoomEvents({
133
144
  room_id: session.room_id,
134
145
  agent_id: identity.agent_id,
@@ -1,7 +1,20 @@
1
1
  import { syncInstalledSkills } from "../skill-install.js";
2
+ import { runFirstRunMcpMigration } from "../update-migration.js";
3
+ import { detectInstallSource, resolveCurrentBinaryPath } from "../self-update.js";
2
4
  import { isKnownHarnessCliEnv } from "./identity.js";
3
5
  import { getCommand } from "./registry.js";
4
- export function runStartupMaintenance(parsed, env = process.env) {
6
+ export async function runStartupMaintenance(parsed, cliEntryUrl, env = process.env) {
7
+ if (shouldRunFirstRunMcpMigration(parsed, cliEntryUrl, env)) {
8
+ try {
9
+ await runFirstRunMcpMigration({
10
+ installOptions: { env }
11
+ });
12
+ }
13
+ catch {
14
+ // Startup cleanup is best-effort. Explicit install, uninstall, and
15
+ // self-update paths surface cleanup failures directly.
16
+ }
17
+ }
5
18
  if (!shouldAutoSyncInstalledSkills(parsed, env)) {
6
19
  return;
7
20
  }
@@ -13,6 +26,19 @@ export function runStartupMaintenance(parsed, env = process.env) {
13
26
  // unrelated tt command fail.
14
27
  }
15
28
  }
29
+ export function shouldRunFirstRunMcpMigration(parsed, cliEntryUrl, env = process.env) {
30
+ if (env.TALKING_STICK_DISABLE_MCP_MIGRATION?.trim()) {
31
+ return false;
32
+ }
33
+ const command = getCommand(parsed.name);
34
+ if (!command?.startupMaintenance) {
35
+ return false;
36
+ }
37
+ const source = detectInstallSource({
38
+ binaryPath: resolveCurrentBinaryPath(cliEntryUrl)
39
+ });
40
+ return source !== "dev" && source !== "unknown";
41
+ }
16
42
  export function shouldAutoSyncInstalledSkills(parsed, env = process.env) {
17
43
  if (env.TALKING_STICK_DISABLE_SKILL_SYNC?.trim()) {
18
44
  return false;
package/dist/cli.js CHANGED
@@ -11,10 +11,10 @@ import { runStartupMaintenance } from "./cli/startup-maintenance.js";
11
11
  export { checkGuardianLiveness } from "./cli/guardian.js";
12
12
  export { parseHandoffJson } from "./cli/handoff.js";
13
13
  export { formatRelativeTime, shouldUseJson } from "./cli/output.js";
14
- export { shouldAutoSyncInstalledSkills } from "./cli/startup-maintenance.js";
14
+ export { shouldAutoSyncInstalledSkills, shouldRunFirstRunMcpMigration } from "./cli/startup-maintenance.js";
15
15
  export async function runCli(argv = process.argv.slice(2)) {
16
16
  const parsed = parseCommand(argv);
17
- runStartupMaintenance(parsed);
17
+ await runStartupMaintenance(parsed, import.meta.url);
18
18
  if (!parsed.name || parsed.name === "help" || parsed.name === "--help") {
19
19
  printHelp();
20
20
  return;
package/dist/commands.js CHANGED
@@ -82,6 +82,21 @@ export class TalkingStickCommands {
82
82
  getRoomEvents(input) {
83
83
  return this.service.getRoomEvents(input);
84
84
  }
85
+ sendMessage(identity, input) {
86
+ return this.service.sendMessage({
87
+ agent_id: identity.agent_id,
88
+ room_id: input.room_id,
89
+ body: input.body,
90
+ to_agent_id: input.to_agent_id,
91
+ delivery_hint: input.delivery_hint
92
+ });
93
+ }
94
+ waitForEvents(input) {
95
+ return this.service.waitForEvents(input);
96
+ }
97
+ getLatestEventSeq(input) {
98
+ return this.service.getLatestEventSeq(input);
99
+ }
85
100
  addNote(identity, input) {
86
101
  return this.service.addNote({
87
102
  agent_id: identity.agent_id,
package/dist/config.js CHANGED
@@ -4,8 +4,11 @@ export const defaultPolicy = {
4
4
  ownerLeaseTtlMs: 45 * 60 * 1000,
5
5
  heartbeatIntervalMs: 5 * 60 * 1000,
6
6
  claimTtlMs: 20 * 60 * 1000,
7
- waitForTurnMaxWaitMs: 30 * 1000,
7
+ waitForTurnMaxWaitMs: 110 * 1000,
8
8
  waitForTurnPollMs: 250,
9
+ waitForEventsMaxWaitMs: 110 * 1000,
10
+ waitForEventsPollMs: 250,
11
+ waitForEventsBatchLimit: 100,
9
12
  presenceTtlMs: 4 * 60 * 60 * 1000,
10
13
  waiterGraceMs: 10 * 1000,
11
14
  idleRoomTtlMs: 7 * 24 * 60 * 60 * 1000
package/dist/db.js CHANGED
@@ -96,6 +96,13 @@ const migrations = [
96
96
  up: `
97
97
  ALTER TABLE room_members ADD COLUMN last_wait_at TEXT;
98
98
  `
99
+ },
100
+ {
101
+ id: 5,
102
+ name: "room_events_payload_json",
103
+ up: `
104
+ ALTER TABLE room_events ADD COLUMN payload_json TEXT;
105
+ `
99
106
  }
100
107
  ];
101
108
  export function resolveDatabasePath(options = {}) {
package/dist/identity.js CHANGED
@@ -127,13 +127,13 @@ function harnessAgentId(harness, sessionId, hostId, username) {
127
127
  function resolveHarnessSessionId(signal, env, parentPid, parentInspection, username, hostId, inspector) {
128
128
  if (signal.sessionId)
129
129
  return `harness:${signal.sessionId}`;
130
- const terminalId = resolveTerminalSessionId(env);
131
- if (terminalId)
132
- return terminalId;
133
130
  const harnessRoot = findHarnessRootInAncestry(signal.harness, parentPid, parentInspection, inspector);
134
131
  if (harnessRoot) {
135
132
  return `pid:${harnessRoot.pid}@${harnessRoot.startTime}`;
136
133
  }
134
+ const terminalId = resolveTerminalSessionId(env);
135
+ if (terminalId)
136
+ return terminalId;
137
137
  if (parentInspection?.startTime) {
138
138
  return `pid:${parentPid}@${parentInspection.startTime}`;
139
139
  }
@@ -224,7 +224,7 @@ function detectHarnessSignal(env) {
224
224
  if (env.CLAUDECODE === "1") {
225
225
  return {
226
226
  harness: "claude",
227
- sessionId: null,
227
+ sessionId: nonEmpty(env.CLAUDE_CODE_SESSION_ID),
228
228
  pidHint: parsePositiveInteger(env.CMUX_CLAUDE_PID)
229
229
  };
230
230
  }
package/dist/index.js CHANGED
@@ -4,9 +4,9 @@ export { applyPragmas, assertLocalFilesystem, detectFilesystemType, migrate, ope
4
4
  export { ProtocolError, isProtocolError } from "./errors.js";
5
5
  export { deriveHarnessCliIdentity, deriveHumanCliIdentity, deriveMcpHarnessIdentity } from "./identity.js";
6
6
  export { ancestorPaths, canonicalizeContextPath, resolveContextPath, resolveWorkspaceRoot } from "./path-resolution.js";
7
- export { createMcpServer, runStdioServer } from "./mcp-server.js";
8
- export { SUPPORTED_HARNESSES, MissingHarnessError, detectHarness, parseHarnessList, planInstall, planUninstall, resolveHarnessConfigDir, resolveOpencodeConfigDir, resolveOpencodeConfigPath, runAction, skipAction } from "./install.js";
7
+ export { SUPPORTED_HARNESSES, MissingHarnessError, detectHarness, parseHarnessList, planUninstall, resolveHarnessConfigDir, resolveOpencodeConfigDir, resolveOpencodeConfigPath, runAction, skipAction } from "./install.js";
9
8
  export { DEFAULT_SKILL_NAME, planSkillInstall, planSkillUninstall, resolveBundledSkillPath, resolveSkillTargetPath, syncInstalledSkills } from "./skill-install.js";
9
+ export { readPackageVersion, readUpdateMigrationState, resolveUpdateMigrationStatePath, runFirstRunMcpMigration, runStaleMcpCleanup, writeUpdateMigrationState } from "./update-migration.js";
10
10
  export { createSystemProcessInspector, terminateKnownProcess } from "./process-utils.js";
11
11
  export { clearCliSessionLease, findCliSessionByRoom, findCliSessionForContextPath, readCliSessions, removeCliSession, removeCliSessionsForRoom, resolveCliSessionPath, upsertCliSession, upsertJoinedCliSession, writeCliSessions } from "./session-store.js";
12
12
  export { TalkingStickService, createDefaultProcessLivenessChecker } from "./service.js";
@@ -0,0 +1,21 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ export class FileAuditLog {
4
+ filePath;
5
+ constructor(filePath) {
6
+ this.filePath = filePath;
7
+ }
8
+ append(entry) {
9
+ const fullEntry = { ts: entry.ts ?? new Date().toISOString(), ...entry };
10
+ fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
11
+ fs.appendFileSync(this.filePath, `${JSON.stringify(fullEntry)}\n`, "utf8");
12
+ }
13
+ }
14
+ export class NoopAuditLog {
15
+ append() {
16
+ // intentionally blank
17
+ }
18
+ }
19
+ export function defaultAuditLogPath(dataDir) {
20
+ return path.join(dataDir, "update-migrations.log");
21
+ }
@@ -0,0 +1,84 @@
1
+ import { SUPPORTED_HARNESSES, planUninstall, runAction } from "./install.js";
2
+ import { NoopAuditLog } from "./install-audit.js";
3
+ export async function removeStaleMcpRegistrations(options) {
4
+ const audit = options.audit ?? new NoopAuditLog();
5
+ const strict = options.strict ?? true;
6
+ const harnesses = options.harnesses === undefined || options.harnesses === "all"
7
+ ? [...SUPPORTED_HARNESSES]
8
+ : options.harnesses;
9
+ const installOptions = {
10
+ skipMissing: true,
11
+ ...(options.installOptions ?? {})
12
+ };
13
+ const results = [];
14
+ for (const harness of harnesses) {
15
+ const result = await removeOneHarness(harness, installOptions, strict);
16
+ results.push(result);
17
+ audit.append({
18
+ reason: options.reason,
19
+ package_version_from: options.packageVersionFrom,
20
+ package_version_to: options.packageVersionTo,
21
+ harness,
22
+ config_path: result.configPath,
23
+ action: result.action,
24
+ server_name: result.serverName,
25
+ detail: result.message
26
+ });
27
+ }
28
+ return results.map(({ harness, action, message }) => ({ harness, action, message }));
29
+ }
30
+ async function removeOneHarness(harness, installOptions, strict) {
31
+ const action = planUninstall(harness, installOptions);
32
+ if (action.kind === "skip") {
33
+ return {
34
+ harness,
35
+ action: "skipped",
36
+ message: action.message,
37
+ serverName: installOptions.serverName ?? "talking-stick"
38
+ };
39
+ }
40
+ if (action.kind === "file-patch") {
41
+ const state = action.inspect ? action.inspect() : "unknown";
42
+ const serverName = action.serverName ?? "talking-stick";
43
+ if (state === "absent") {
44
+ return {
45
+ harness,
46
+ action: "absent",
47
+ message: `${harness}: no Talking Stick MCP entry to remove`,
48
+ configPath: action.filePath,
49
+ serverName
50
+ };
51
+ }
52
+ if (strict && state !== "present") {
53
+ return {
54
+ harness,
55
+ action: "preserved",
56
+ message: `${harness}: hand-edited entry left alone (state=${state})`,
57
+ configPath: action.filePath,
58
+ serverName
59
+ };
60
+ }
61
+ }
62
+ const installResult = await runAction(action, installOptions);
63
+ return mapInstallResult(harness, action, installResult);
64
+ }
65
+ function mapInstallResult(harness, action, result) {
66
+ let serverName = "talking-stick";
67
+ if ("serverName" in action && typeof action.serverName === "string") {
68
+ serverName = action.serverName;
69
+ }
70
+ const configPath = action.kind === "file-patch" ? action.filePath : undefined;
71
+ if (!result.ok) {
72
+ return { harness, action: "failed", message: result.message, configPath, serverName };
73
+ }
74
+ switch (result.status) {
75
+ case "already_absent":
76
+ return { harness, action: "absent", message: result.message, configPath, serverName };
77
+ case "removed":
78
+ return { harness, action: "removed", message: result.message, configPath, serverName };
79
+ case "skipped":
80
+ return { harness, action: "skipped", message: result.message, configPath, serverName };
81
+ default:
82
+ return { harness, action: "failed", message: result.message, configPath, serverName };
83
+ }
84
+ }
package/dist/install.js CHANGED
@@ -115,75 +115,6 @@ function resolveHarnessConfigDirFromResolved(harness, resolved) {
115
115
  throw new Error(`Unknown harness: ${harness}`);
116
116
  }
117
117
  }
118
- export function planInstall(harness, options = {}) {
119
- const resolved = resolveOptions(options);
120
- const [serverBin, ...serverArgs] = resolved.serverCommand;
121
- if (!serverBin)
122
- throw new Error("serverCommand must include at least the binary");
123
- switch (harness) {
124
- case "claude-code":
125
- if (resolved.skipMissing && !resolved.hooks.which("claude")) {
126
- return skipAction(harness, "claude not on PATH");
127
- }
128
- return {
129
- kind: "exec",
130
- harness,
131
- command: "claude",
132
- args: ["mcp", "add", "-s", "user", resolved.serverName, "--", serverBin, ...serverArgs],
133
- description: `claude mcp add -s user ${resolved.serverName} -- ${resolved.serverCommand.join(" ")}`,
134
- operation: "install",
135
- serverName: resolved.serverName,
136
- serverCommand: resolved.serverCommand
137
- };
138
- case "codex":
139
- if (resolved.skipMissing && !resolved.hooks.which("codex")) {
140
- return skipAction(harness, "codex not on PATH");
141
- }
142
- return {
143
- kind: "exec",
144
- harness,
145
- command: "codex",
146
- args: ["mcp", "add", resolved.serverName, "--", serverBin, ...serverArgs],
147
- description: `codex mcp add ${resolved.serverName} -- ${resolved.serverCommand.join(" ")}`,
148
- operation: "install",
149
- serverName: resolved.serverName,
150
- serverCommand: resolved.serverCommand
151
- };
152
- case "gemini":
153
- if (resolved.skipMissing && !resolved.hooks.which("gemini")) {
154
- return skipAction(harness, "gemini not on PATH");
155
- }
156
- return {
157
- kind: "exec",
158
- harness,
159
- command: "gemini",
160
- args: ["mcp", "add", "-s", "user", "-t", "stdio", resolved.serverName, serverBin, ...serverArgs],
161
- description: `gemini mcp add -s user -t stdio ${resolved.serverName} ${resolved.serverCommand.join(" ")}`,
162
- operation: "install",
163
- serverName: resolved.serverName,
164
- serverCommand: resolved.serverCommand
165
- };
166
- case "opencode": {
167
- const filePath = resolveOpencodeConfigPath(options);
168
- const configDir = path.dirname(filePath);
169
- if (resolved.skipMissing && !resolved.hooks.pathExists(configDir)) {
170
- return skipAction(harness, `opencode config directory not found: ${configDir}`);
171
- }
172
- return {
173
- kind: "file-patch",
174
- harness,
175
- filePath,
176
- description: `merge mcp.${resolved.serverName} into ${filePath}`,
177
- operation: "install",
178
- serverName: resolved.serverName,
179
- inspect: () => inspectOpencodeConfig(filePath, resolved),
180
- apply: () => patchOpencodeConfig(filePath, resolved, "install")
181
- };
182
- }
183
- default:
184
- throw new Error(`Unknown harness: ${harness}`);
185
- }
186
- }
187
118
  export function planUninstall(harness, options = {}) {
188
119
  const resolved = resolveOptions(options);
189
120
  switch (harness) {