talking-stick 0.1.0-alpha.5 → 0.1.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.
- package/README.md +2 -2
- package/dist/cli/install-commands.js +4 -3
- package/dist/install.js +222 -14
- package/dist/mcp-server.js +1 -1
- package/docs/releases/0.1.0-alpha.6.md +61 -0
- package/docs/releases/0.1.0.md +37 -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
|
An MCP coordination server 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.1.0
|
|
5
|
+
**Version:** 0.1.0. Multi-process-safe (SQLite WAL), liveness-aware, no daemon. Supports Claude Code, Codex CLI, Gemini CLI, and OpenCode out of the box.
|
|
6
6
|
|
|
7
7
|
## Quickstart
|
|
8
8
|
|
|
@@ -27,7 +27,7 @@ That's it. The next time two agents `cd` into the same repo, they see each other
|
|
|
27
27
|
|
|
28
28
|
| Method | Command | Notes |
|
|
29
29
|
|---|---|---|
|
|
30
|
-
| **From npm** | `npm i -g talking-stick` | Published as `0.1.0
|
|
30
|
+
| **From npm** | `npm i -g talking-stick` | Published as `0.1.0`. Requires Node ≥ 22. |
|
|
31
31
|
| **From GitHub** | `npm i -g github:mostlydev/talking-stick` | Tracks the `master` branch; builds on install via the `prepare` hook. |
|
|
32
32
|
| **From source** | `git clone … && npm install && npm link` | For contributors. |
|
|
33
33
|
|
|
@@ -143,9 +143,7 @@ function runInheritIo(command, args) {
|
|
|
143
143
|
function reportInstallResults(results, mode) {
|
|
144
144
|
let anyFailed = false;
|
|
145
145
|
for (const result of results) {
|
|
146
|
-
|
|
147
|
-
continue;
|
|
148
|
-
const status = result.ok ? "ok" : "FAIL";
|
|
146
|
+
const status = formatInstallStatus(result.status);
|
|
149
147
|
process.stdout.write(`[${result.harness}] ${status}: ${result.message}\n`);
|
|
150
148
|
if (!result.ok)
|
|
151
149
|
anyFailed = true;
|
|
@@ -154,3 +152,6 @@ function reportInstallResults(results, mode) {
|
|
|
154
152
|
throw new Error(`${mode} completed with failures.`);
|
|
155
153
|
}
|
|
156
154
|
}
|
|
155
|
+
function formatInstallStatus(status) {
|
|
156
|
+
return status.replaceAll("_", "-");
|
|
157
|
+
}
|
package/dist/install.js
CHANGED
|
@@ -130,7 +130,10 @@ export function planInstall(harness, options = {}) {
|
|
|
130
130
|
harness,
|
|
131
131
|
command: "claude",
|
|
132
132
|
args: ["mcp", "add", "-s", "user", resolved.serverName, "--", serverBin, ...serverArgs],
|
|
133
|
-
description: `claude mcp add -s user ${resolved.serverName} -- ${resolved.serverCommand.join(" ")}
|
|
133
|
+
description: `claude mcp add -s user ${resolved.serverName} -- ${resolved.serverCommand.join(" ")}`,
|
|
134
|
+
operation: "install",
|
|
135
|
+
serverName: resolved.serverName,
|
|
136
|
+
serverCommand: resolved.serverCommand
|
|
134
137
|
};
|
|
135
138
|
case "codex":
|
|
136
139
|
if (resolved.skipMissing && !resolved.hooks.which("codex")) {
|
|
@@ -141,7 +144,10 @@ export function planInstall(harness, options = {}) {
|
|
|
141
144
|
harness,
|
|
142
145
|
command: "codex",
|
|
143
146
|
args: ["mcp", "add", resolved.serverName, "--", serverBin, ...serverArgs],
|
|
144
|
-
description: `codex mcp add ${resolved.serverName} -- ${resolved.serverCommand.join(" ")}
|
|
147
|
+
description: `codex mcp add ${resolved.serverName} -- ${resolved.serverCommand.join(" ")}`,
|
|
148
|
+
operation: "install",
|
|
149
|
+
serverName: resolved.serverName,
|
|
150
|
+
serverCommand: resolved.serverCommand
|
|
145
151
|
};
|
|
146
152
|
case "gemini":
|
|
147
153
|
if (resolved.skipMissing && !resolved.hooks.which("gemini")) {
|
|
@@ -152,7 +158,10 @@ export function planInstall(harness, options = {}) {
|
|
|
152
158
|
harness,
|
|
153
159
|
command: "gemini",
|
|
154
160
|
args: ["mcp", "add", "-s", "user", "-t", "stdio", resolved.serverName, serverBin, ...serverArgs],
|
|
155
|
-
description: `gemini mcp add -s user -t stdio ${resolved.serverName} ${resolved.serverCommand.join(" ")}
|
|
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
|
|
156
165
|
};
|
|
157
166
|
case "opencode": {
|
|
158
167
|
const filePath = resolveOpencodeConfigPath(options);
|
|
@@ -165,6 +174,9 @@ export function planInstall(harness, options = {}) {
|
|
|
165
174
|
harness,
|
|
166
175
|
filePath,
|
|
167
176
|
description: `merge mcp.${resolved.serverName} into ${filePath}`,
|
|
177
|
+
operation: "install",
|
|
178
|
+
serverName: resolved.serverName,
|
|
179
|
+
inspect: () => inspectOpencodeConfig(filePath, resolved),
|
|
168
180
|
apply: () => patchOpencodeConfig(filePath, resolved, "install")
|
|
169
181
|
};
|
|
170
182
|
}
|
|
@@ -184,7 +196,9 @@ export function planUninstall(harness, options = {}) {
|
|
|
184
196
|
harness,
|
|
185
197
|
command: "claude",
|
|
186
198
|
args: ["mcp", "remove", "-s", "user", resolved.serverName],
|
|
187
|
-
description: `claude mcp remove -s user ${resolved.serverName}
|
|
199
|
+
description: `claude mcp remove -s user ${resolved.serverName}`,
|
|
200
|
+
operation: "uninstall",
|
|
201
|
+
serverName: resolved.serverName
|
|
188
202
|
};
|
|
189
203
|
case "codex":
|
|
190
204
|
if (resolved.skipMissing && !resolved.hooks.which("codex")) {
|
|
@@ -195,7 +209,9 @@ export function planUninstall(harness, options = {}) {
|
|
|
195
209
|
harness,
|
|
196
210
|
command: "codex",
|
|
197
211
|
args: ["mcp", "remove", resolved.serverName],
|
|
198
|
-
description: `codex mcp remove ${resolved.serverName}
|
|
212
|
+
description: `codex mcp remove ${resolved.serverName}`,
|
|
213
|
+
operation: "uninstall",
|
|
214
|
+
serverName: resolved.serverName
|
|
199
215
|
};
|
|
200
216
|
case "gemini":
|
|
201
217
|
if (resolved.skipMissing && !resolved.hooks.which("gemini")) {
|
|
@@ -206,7 +222,9 @@ export function planUninstall(harness, options = {}) {
|
|
|
206
222
|
harness,
|
|
207
223
|
command: "gemini",
|
|
208
224
|
args: ["mcp", "remove", "-s", "user", resolved.serverName],
|
|
209
|
-
description: `gemini mcp remove -s user ${resolved.serverName}
|
|
225
|
+
description: `gemini mcp remove -s user ${resolved.serverName}`,
|
|
226
|
+
operation: "uninstall",
|
|
227
|
+
serverName: resolved.serverName
|
|
210
228
|
};
|
|
211
229
|
case "opencode": {
|
|
212
230
|
const filePath = resolveOpencodeConfigPath(options);
|
|
@@ -222,6 +240,9 @@ export function planUninstall(harness, options = {}) {
|
|
|
222
240
|
harness,
|
|
223
241
|
filePath,
|
|
224
242
|
description: `remove mcp.${resolved.serverName} from ${filePath}`,
|
|
243
|
+
operation: "uninstall",
|
|
244
|
+
serverName: resolved.serverName,
|
|
245
|
+
inspect: () => inspectOpencodeConfig(filePath, resolved),
|
|
225
246
|
apply: () => patchOpencodeConfig(filePath, resolved, "uninstall")
|
|
226
247
|
};
|
|
227
248
|
}
|
|
@@ -264,6 +285,47 @@ function patchOpencodeConfig(filePath, resolved, mode) {
|
|
|
264
285
|
resolved.hooks.ensureDir(path.dirname(filePath));
|
|
265
286
|
resolved.hooks.writeFile(filePath, JSON.stringify(config, null, 2) + "\n");
|
|
266
287
|
}
|
|
288
|
+
function inspectOpencodeConfig(filePath, resolved) {
|
|
289
|
+
const existing = resolved.hooks.readFile(filePath);
|
|
290
|
+
if (existing === null)
|
|
291
|
+
return "absent";
|
|
292
|
+
let config;
|
|
293
|
+
try {
|
|
294
|
+
config = parseJsonOrThrow(existing, filePath);
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
return "unknown";
|
|
298
|
+
}
|
|
299
|
+
const mcp = isPlainObject(config.mcp) ? config.mcp : {};
|
|
300
|
+
if (!(resolved.serverName in mcp))
|
|
301
|
+
return "absent";
|
|
302
|
+
const expected = {
|
|
303
|
+
type: "local",
|
|
304
|
+
command: [...resolved.serverCommand],
|
|
305
|
+
enabled: true
|
|
306
|
+
};
|
|
307
|
+
return valuesEqual(mcp[resolved.serverName], expected) ? "present" : "different";
|
|
308
|
+
}
|
|
309
|
+
function inspectGeminiSettings(action, resolved) {
|
|
310
|
+
const filePath = path.join(resolveHarnessConfigDirFromResolved("gemini", resolved), "settings.json");
|
|
311
|
+
const existing = resolved.hooks.readFile(filePath);
|
|
312
|
+
if (existing === null)
|
|
313
|
+
return "absent";
|
|
314
|
+
let config;
|
|
315
|
+
try {
|
|
316
|
+
config = parseJsonOrThrow(existing, filePath);
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return "unknown";
|
|
320
|
+
}
|
|
321
|
+
const servers = isPlainObject(config.mcpServers) ? config.mcpServers : {};
|
|
322
|
+
const serverName = action.serverName ?? resolved.serverName;
|
|
323
|
+
if (!(serverName in servers))
|
|
324
|
+
return "absent";
|
|
325
|
+
const [command, ...args] = action.serverCommand ?? resolved.serverCommand;
|
|
326
|
+
const expected = { command, args };
|
|
327
|
+
return valuesEqual(servers[serverName], expected) ? "present" : "different";
|
|
328
|
+
}
|
|
267
329
|
function parseJsonOrThrow(raw, filePath) {
|
|
268
330
|
try {
|
|
269
331
|
const parsed = JSON.parse(raw);
|
|
@@ -279,6 +341,9 @@ function parseJsonOrThrow(raw, filePath) {
|
|
|
279
341
|
function isPlainObject(value) {
|
|
280
342
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
281
343
|
}
|
|
344
|
+
function valuesEqual(left, right) {
|
|
345
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
346
|
+
}
|
|
282
347
|
export function detectHarness(harness, options = {}) {
|
|
283
348
|
const resolved = resolveOptions(options);
|
|
284
349
|
switch (harness) {
|
|
@@ -333,6 +398,7 @@ export async function runAction(action, options = {}) {
|
|
|
333
398
|
harness: action.harness,
|
|
334
399
|
ok: true,
|
|
335
400
|
action,
|
|
401
|
+
status: "skipped",
|
|
336
402
|
message: action.message,
|
|
337
403
|
skipped: true
|
|
338
404
|
};
|
|
@@ -344,6 +410,7 @@ export async function runAction(action, options = {}) {
|
|
|
344
410
|
harness: action.harness,
|
|
345
411
|
ok: resolved.skipMissing,
|
|
346
412
|
action,
|
|
413
|
+
status: resolved.skipMissing ? "skipped" : "failed",
|
|
347
414
|
message: `${action.command} not on PATH`,
|
|
348
415
|
skipped: resolved.skipMissing
|
|
349
416
|
};
|
|
@@ -353,9 +420,29 @@ export async function runAction(action, options = {}) {
|
|
|
353
420
|
harness: action.harness,
|
|
354
421
|
ok: false,
|
|
355
422
|
action,
|
|
423
|
+
status: "failed",
|
|
356
424
|
message: invocation.error
|
|
357
425
|
};
|
|
358
426
|
}
|
|
427
|
+
const beforeState = await inspectExecAction(action, resolved);
|
|
428
|
+
if (action.operation === "install" && beforeState === "present") {
|
|
429
|
+
return {
|
|
430
|
+
harness: action.harness,
|
|
431
|
+
ok: true,
|
|
432
|
+
action,
|
|
433
|
+
status: "already_present",
|
|
434
|
+
message: formatMcpActionMessage(action, "already_present")
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
if (action.operation === "uninstall" && beforeState === "absent") {
|
|
438
|
+
return {
|
|
439
|
+
harness: action.harness,
|
|
440
|
+
ok: true,
|
|
441
|
+
action,
|
|
442
|
+
status: "already_absent",
|
|
443
|
+
message: formatMcpActionMessage(action, "already_absent")
|
|
444
|
+
};
|
|
445
|
+
}
|
|
359
446
|
let result;
|
|
360
447
|
try {
|
|
361
448
|
result = await resolved.hooks.run(invocation.command, invocation.args, invocation.options);
|
|
@@ -365,31 +452,75 @@ export async function runAction(action, options = {}) {
|
|
|
365
452
|
harness: action.harness,
|
|
366
453
|
ok: false,
|
|
367
454
|
action,
|
|
455
|
+
status: "failed",
|
|
368
456
|
message: formatExecError(error)
|
|
369
457
|
};
|
|
370
458
|
}
|
|
371
459
|
if (result.exitCode === 0) {
|
|
460
|
+
const status = successStatusForOperation(action.operation, beforeState);
|
|
461
|
+
return {
|
|
462
|
+
harness: action.harness,
|
|
463
|
+
ok: true,
|
|
464
|
+
action,
|
|
465
|
+
status,
|
|
466
|
+
message: formatMcpActionMessage(action, status, result.stdout.trim() || undefined)
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
const errorMessage = result.stderr.trim() || result.stdout.trim() || `${action.command} exited with code ${result.exitCode}`;
|
|
470
|
+
if (action.operation === "install" && isAlreadyPresentMessage(errorMessage)) {
|
|
471
|
+
return {
|
|
472
|
+
harness: action.harness,
|
|
473
|
+
ok: true,
|
|
474
|
+
action,
|
|
475
|
+
status: "already_present",
|
|
476
|
+
message: formatMcpActionMessage(action, "already_present")
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
if (action.operation === "uninstall" && isAlreadyAbsentMessage(errorMessage)) {
|
|
372
480
|
return {
|
|
373
481
|
harness: action.harness,
|
|
374
482
|
ok: true,
|
|
375
483
|
action,
|
|
376
|
-
|
|
484
|
+
status: "already_absent",
|
|
485
|
+
message: formatMcpActionMessage(action, "already_absent")
|
|
377
486
|
};
|
|
378
487
|
}
|
|
379
488
|
return {
|
|
380
489
|
harness: action.harness,
|
|
381
490
|
ok: false,
|
|
382
491
|
action,
|
|
383
|
-
|
|
492
|
+
status: "failed",
|
|
493
|
+
message: errorMessage
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
const beforeState = action.inspect?.() ?? "unknown";
|
|
497
|
+
if (action.operation === "install" && beforeState === "present") {
|
|
498
|
+
return {
|
|
499
|
+
harness: action.harness,
|
|
500
|
+
ok: true,
|
|
501
|
+
action,
|
|
502
|
+
status: "already_present",
|
|
503
|
+
message: formatMcpActionMessage(action, "already_present")
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
if (action.operation === "uninstall" && beforeState === "absent") {
|
|
507
|
+
return {
|
|
508
|
+
harness: action.harness,
|
|
509
|
+
ok: true,
|
|
510
|
+
action,
|
|
511
|
+
status: "already_absent",
|
|
512
|
+
message: formatMcpActionMessage(action, "already_absent")
|
|
384
513
|
};
|
|
385
514
|
}
|
|
386
515
|
try {
|
|
387
516
|
action.apply();
|
|
517
|
+
const status = successStatusForOperation(action.operation, beforeState);
|
|
388
518
|
return {
|
|
389
519
|
harness: action.harness,
|
|
390
520
|
ok: true,
|
|
391
521
|
action,
|
|
392
|
-
|
|
522
|
+
status,
|
|
523
|
+
message: formatMcpActionMessage(action, status, `Updated ${action.filePath}`)
|
|
393
524
|
};
|
|
394
525
|
}
|
|
395
526
|
catch (error) {
|
|
@@ -398,6 +529,7 @@ export async function runAction(action, options = {}) {
|
|
|
398
529
|
harness: action.harness,
|
|
399
530
|
ok: true,
|
|
400
531
|
action,
|
|
532
|
+
status: "skipped",
|
|
401
533
|
message: error.message,
|
|
402
534
|
skipped: true
|
|
403
535
|
};
|
|
@@ -406,10 +538,83 @@ export async function runAction(action, options = {}) {
|
|
|
406
538
|
harness: action.harness,
|
|
407
539
|
ok: false,
|
|
408
540
|
action,
|
|
541
|
+
status: "failed",
|
|
409
542
|
message: error.message
|
|
410
543
|
};
|
|
411
544
|
}
|
|
412
545
|
}
|
|
546
|
+
async function inspectExecAction(action, resolved) {
|
|
547
|
+
if (!action.operation || !action.serverName)
|
|
548
|
+
return "unknown";
|
|
549
|
+
if (action.harness === "gemini") {
|
|
550
|
+
return inspectGeminiSettings(action, resolved);
|
|
551
|
+
}
|
|
552
|
+
if (action.harness !== "claude-code" && action.harness !== "codex") {
|
|
553
|
+
return "unknown";
|
|
554
|
+
}
|
|
555
|
+
const invocation = resolveCommandInvocation(action.command, ["mcp", "get", action.serverName], resolved);
|
|
556
|
+
if (!invocation || "error" in invocation)
|
|
557
|
+
return "unknown";
|
|
558
|
+
try {
|
|
559
|
+
const result = await resolved.hooks.run(invocation.command, invocation.args, invocation.options);
|
|
560
|
+
return result.exitCode === 0 ? "present" : "absent";
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
return "unknown";
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
function successStatusForOperation(operation, beforeState) {
|
|
567
|
+
if (operation === "install") {
|
|
568
|
+
return beforeState === "different" ? "updated" : "added";
|
|
569
|
+
}
|
|
570
|
+
if (operation === "uninstall") {
|
|
571
|
+
return "removed";
|
|
572
|
+
}
|
|
573
|
+
return "ok";
|
|
574
|
+
}
|
|
575
|
+
function formatMcpActionMessage(action, status, fallback) {
|
|
576
|
+
if (!action.serverName || !action.operation) {
|
|
577
|
+
return fallback ?? "ok";
|
|
578
|
+
}
|
|
579
|
+
const target = `MCP server '${action.serverName}'`;
|
|
580
|
+
const location = mcpConfigLocation(action);
|
|
581
|
+
switch (status) {
|
|
582
|
+
case "added":
|
|
583
|
+
return `${target} registered in ${location}.`;
|
|
584
|
+
case "updated":
|
|
585
|
+
return `${target} updated in ${location}.`;
|
|
586
|
+
case "already_present":
|
|
587
|
+
return `${target} already registered in ${location}.`;
|
|
588
|
+
case "removed":
|
|
589
|
+
return `${target} removed from ${location}.`;
|
|
590
|
+
case "already_absent":
|
|
591
|
+
return `${target} is not registered in ${location}.`;
|
|
592
|
+
default:
|
|
593
|
+
return fallback ?? "ok";
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
function mcpConfigLocation(action) {
|
|
597
|
+
if (action.kind === "file-patch")
|
|
598
|
+
return action.filePath;
|
|
599
|
+
switch (action.harness) {
|
|
600
|
+
case "claude-code":
|
|
601
|
+
return "Claude Code user config";
|
|
602
|
+
case "codex":
|
|
603
|
+
return "Codex global config";
|
|
604
|
+
case "gemini":
|
|
605
|
+
return "Gemini user config";
|
|
606
|
+
case "opencode":
|
|
607
|
+
return "OpenCode config";
|
|
608
|
+
default:
|
|
609
|
+
return "harness config";
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
function isAlreadyPresentMessage(message) {
|
|
613
|
+
return /\balready\b.*\b(exists|configured|present|registered|installed)\b/i.test(message);
|
|
614
|
+
}
|
|
615
|
+
function isAlreadyAbsentMessage(message) {
|
|
616
|
+
return /\b(not found|does not exist|not configured|not registered|no mcp server)\b/i.test(message);
|
|
617
|
+
}
|
|
413
618
|
export function parseHarnessList(values) {
|
|
414
619
|
const result = [];
|
|
415
620
|
for (const value of values) {
|
|
@@ -422,15 +627,18 @@ export function parseHarnessList(values) {
|
|
|
422
627
|
return result;
|
|
423
628
|
}
|
|
424
629
|
function resolveExecInvocation(action, resolved) {
|
|
425
|
-
|
|
630
|
+
return resolveCommandInvocation(action.command, action.args, resolved);
|
|
631
|
+
}
|
|
632
|
+
function resolveCommandInvocation(command, args, resolved) {
|
|
633
|
+
const executable = resolved.hooks.which(command);
|
|
426
634
|
if (!executable) {
|
|
427
635
|
return null;
|
|
428
636
|
}
|
|
429
637
|
if (resolved.platform === "win32" && /\.(cmd|bat)$/i.test(executable)) {
|
|
430
|
-
const unsafeArg =
|
|
638
|
+
const unsafeArg = args.find(containsWindowsCmdMetacharacter);
|
|
431
639
|
if (unsafeArg !== undefined) {
|
|
432
640
|
return {
|
|
433
|
-
error: `Cannot safely launch ${
|
|
641
|
+
error: `Cannot safely launch ${command} through cmd.exe because ` +
|
|
434
642
|
`an argument contains Windows command metacharacters (& | < > ^ % ").`
|
|
435
643
|
};
|
|
436
644
|
}
|
|
@@ -439,13 +647,13 @@ function resolveExecInvocation(action, resolved) {
|
|
|
439
647
|
"cmd.exe";
|
|
440
648
|
return {
|
|
441
649
|
command: cmdExe,
|
|
442
|
-
args: ["/d", "/s", "/c", executable, ...
|
|
650
|
+
args: ["/d", "/s", "/c", executable, ...args],
|
|
443
651
|
options: { windowsHide: true }
|
|
444
652
|
};
|
|
445
653
|
}
|
|
446
654
|
return {
|
|
447
655
|
command: executable,
|
|
448
|
-
args
|
|
656
|
+
args,
|
|
449
657
|
options: resolved.platform === "win32" ? { windowsHide: true } : undefined
|
|
450
658
|
};
|
|
451
659
|
}
|
package/dist/mcp-server.js
CHANGED
|
@@ -27,7 +27,7 @@ export function createMcpServer(service = new TalkingStickService()) {
|
|
|
27
27
|
const resolveConnectionIdentity = createConnectionIdentityResolver();
|
|
28
28
|
const server = new McpServer({
|
|
29
29
|
name: "talking-stick",
|
|
30
|
-
version: "0.1.0
|
|
30
|
+
version: "0.1.0"
|
|
31
31
|
});
|
|
32
32
|
server.registerTool("list_rooms", {
|
|
33
33
|
title: "List Rooms",
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Talking Stick 0.1.0-alpha.6
|
|
2
|
+
|
|
3
|
+
Date: 2026-04-27
|
|
4
|
+
|
|
5
|
+
Installer idempotency alpha. This release makes repeated `tt install --all`
|
|
6
|
+
runs precise instead of noisy.
|
|
7
|
+
|
|
8
|
+
## Changed
|
|
9
|
+
|
|
10
|
+
### Precise install results
|
|
11
|
+
|
|
12
|
+
`tt install` and `tt uninstall` now return and print explicit result statuses:
|
|
13
|
+
|
|
14
|
+
- `added`
|
|
15
|
+
- `already-present`
|
|
16
|
+
- `updated`
|
|
17
|
+
- `removed`
|
|
18
|
+
- `already-absent`
|
|
19
|
+
- `skipped`
|
|
20
|
+
- `failed`
|
|
21
|
+
|
|
22
|
+
The CLI reports the state Talking Stick observed instead of forwarding each
|
|
23
|
+
harness's native wording directly. That keeps Claude Code, Codex, Gemini, and
|
|
24
|
+
OpenCode output consistent.
|
|
25
|
+
|
|
26
|
+
### Idempotent MCP install preflight
|
|
27
|
+
|
|
28
|
+
Before running native MCP add/remove commands, Talking Stick now checks whether
|
|
29
|
+
the named MCP server is already in the expected state:
|
|
30
|
+
|
|
31
|
+
- Claude Code and Codex use their native `mcp get` command.
|
|
32
|
+
- Gemini is inspected through its user `settings.json`.
|
|
33
|
+
- OpenCode is inspected through its JSON config.
|
|
34
|
+
|
|
35
|
+
If `talking-stick` is already registered, `tt install` reports
|
|
36
|
+
`already-present` and does not call another native add command.
|
|
37
|
+
|
|
38
|
+
## Fixed
|
|
39
|
+
|
|
40
|
+
### Claude Code already-present installs
|
|
41
|
+
|
|
42
|
+
Claude Code reports an existing MCP server as a native command failure. Talking
|
|
43
|
+
Stick now recognizes that response as `already-present`, so a rerun of
|
|
44
|
+
`tt install --all` no longer ends with "install completed with failures" just
|
|
45
|
+
because Claude Code was already configured.
|
|
46
|
+
|
|
47
|
+
### Codex duplicate-looking installs
|
|
48
|
+
|
|
49
|
+
Codex already stores MCP servers by name, but rerunning `tt install --all`
|
|
50
|
+
previously invoked `codex mcp add` again and printed Codex's native "Added"
|
|
51
|
+
message. Talking Stick now reports `already-present` without invoking another
|
|
52
|
+
add when the named server is already present.
|
|
53
|
+
|
|
54
|
+
## Verification
|
|
55
|
+
|
|
56
|
+
- `npm test -- tests/install.test.ts`
|
|
57
|
+
- `npm run typecheck`
|
|
58
|
+
- `npm run build`
|
|
59
|
+
- `git diff --check`
|
|
60
|
+
- `node dist/cli.js install --all`
|
|
61
|
+
- `npx vitest run --reporter verbose` — 208 tests across 14 files
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Talking Stick 0.1.0
|
|
2
|
+
|
|
3
|
+
Date: 2026-04-27
|
|
4
|
+
|
|
5
|
+
First non-alpha release. This promotes the current single-host coordination
|
|
6
|
+
surface from alpha to the initial SemVer baseline.
|
|
7
|
+
|
|
8
|
+
## What is in the baseline
|
|
9
|
+
|
|
10
|
+
- MCP room coordination: `join_path`, `wait_for_turn`, `heartbeat`,
|
|
11
|
+
`release_stick`, `pass_stick`, `takeover_stick`, state reads, event reads,
|
|
12
|
+
and non-owner notes.
|
|
13
|
+
- Human `tt` CLI flows for joining, waiting, releasing, passing, takeover,
|
|
14
|
+
notes, room inspection, installers, skill installers, and self-update.
|
|
15
|
+
- SQLite WAL-backed persistence with multi-process safety, lease fencing,
|
|
16
|
+
liveness-aware recovery, fair release selection, and ephemeral room cleanup.
|
|
17
|
+
- User-global MCP and skill install support for Claude Code, Codex CLI, Gemini
|
|
18
|
+
CLI, and OpenCode.
|
|
19
|
+
|
|
20
|
+
## Changed
|
|
21
|
+
|
|
22
|
+
### Non-alpha package version
|
|
23
|
+
|
|
24
|
+
Package metadata, README version text, and the MCP server handshake now advertise
|
|
25
|
+
`0.1.0`.
|
|
26
|
+
|
|
27
|
+
### Long-poll skill guidance
|
|
28
|
+
|
|
29
|
+
The bundled skill now uses `110000` ms as the default client-safe
|
|
30
|
+
`wait_for_turn` long-poll budget.
|
|
31
|
+
|
|
32
|
+
## Verification
|
|
33
|
+
|
|
34
|
+
- `npm run typecheck`
|
|
35
|
+
- `npm test` — 208 tests across 14 files
|
|
36
|
+
- `git diff --check`
|
|
37
|
+
- `npm pack --dry-run --ignore-scripts`
|
package/package.json
CHANGED
|
@@ -53,11 +53,11 @@ Keep the wait input minimal:
|
|
|
53
53
|
```json
|
|
54
54
|
{
|
|
55
55
|
"room_id": "<room_id from join_path>",
|
|
56
|
-
"max_wait_ms":
|
|
56
|
+
"max_wait_ms": 110000
|
|
57
57
|
}
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
`max_wait_ms` is optional. Use the longest client-safe wait you can support:
|
|
60
|
+
`max_wait_ms` is optional. Use the longest client-safe wait you can support: 110000 ms is a good MCP default when the harness can tolerate it; 180000 ms is fine only when the tool/client timeout is known to exceed that. If the call times out at the harness layer, fall back to a shorter value and call again. Do not send `cursor`, even if an old tool schema still exposes it; `wait_for_turn` is cursor-free, and resumable event replay belongs to `get_room_events`.
|
|
61
61
|
|
|
62
62
|
Possible outcomes:
|
|
63
63
|
|