talking-stick 0.4.8 → 0.4.10

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/dist/identity.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import crypto from "node:crypto";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
+ import { findGrokSessionRecord, resolveGrokSessionLogPath } from "./grok-session-store.js";
5
+ import { resolveContextPath } from "./path-resolution.js";
4
6
  import { createSystemProcessInspector } from "./process-utils.js";
5
7
  const HARNESS_CLI_EXPORT_ENV = "TT_HARNESS_EXPORT";
6
8
  const HARNESS_CLI_AGENT_ID_ENV = "TT_HARNESS_AGENT_ID";
@@ -41,7 +43,11 @@ export function deriveMcpHarnessIdentity(options = {}) {
41
43
  const signal = detectHarnessSignal(env);
42
44
  if (signal) {
43
45
  const processRef = resolveSignalProcessRef(signal, parentPid, parentInspection, inspector);
44
- const sessionId = resolveHarnessSessionId(signal, env, processRef.pid, processRef.inspection, username, hostId, inspector);
46
+ const sessionId = resolveHarnessSessionId(signal, env, processRef.pid, processRef.inspection, username, hostId, inspector, {
47
+ contextPath: options.contextPath,
48
+ grokSessionLogPath: options.grokSessionLogPath,
49
+ now: options.now
50
+ });
45
51
  const harnessProcess = resolveHarnessProcessRef(signal, processRef, inspector);
46
52
  const agentId = options.agentId ?? harnessAgentId(signal.harness, sessionId, hostId, username);
47
53
  return {
@@ -100,7 +106,9 @@ export function deriveHarnessCliIdentity(options = {}) {
100
106
  }
101
107
  let signal = detectHarnessSignal(env);
102
108
  if (!signal && !isHarnessCliExportEnabled(env)) {
103
- return null;
109
+ signal = detectGrokViaAncestry(parentPid, parentInspection, inspector);
110
+ if (!signal)
111
+ return null;
104
112
  }
105
113
  if (!signal) {
106
114
  signal = detectHarnessViaAncestry(parentPid, inspector);
@@ -109,7 +117,11 @@ export function deriveHarnessCliIdentity(options = {}) {
109
117
  return null;
110
118
  const processRef = resolveSignalProcessRef(signal, parentPid, parentInspection, inspector);
111
119
  const username = options.username ?? safeUsername();
112
- const sessionId = resolveHarnessSessionId(signal, env, processRef.pid, processRef.inspection, username, hostId, inspector);
120
+ const sessionId = resolveHarnessSessionId(signal, env, processRef.pid, processRef.inspection, username, hostId, inspector, {
121
+ contextPath: options.contextPath,
122
+ grokSessionLogPath: options.grokSessionLogPath,
123
+ now: options.now
124
+ });
113
125
  const agentId = options.agentId ?? harnessAgentId(signal.harness, sessionId, hostId, username);
114
126
  const harnessProcess = resolveHarnessProcessRef(signal, processRef, inspector);
115
127
  return {
@@ -136,10 +148,16 @@ function harnessAgentId(harness, sessionId, hostId, username) {
136
148
  sanitizeIdentityComponent(username)
137
149
  ])}`;
138
150
  }
139
- function resolveHarnessSessionId(signal, env, parentPid, parentInspection, username, hostId, inspector) {
151
+ function resolveHarnessSessionId(signal, env, parentPid, parentInspection, username, hostId, inspector, options = {}) {
140
152
  if (signal.sessionId)
141
153
  return `harness:${signal.sessionId}`;
142
154
  const harnessRoot = findHarnessRootInAncestry(signal.harness, parentPid, parentInspection, inspector);
155
+ if (signal.harness === "grok") {
156
+ const grokSessionId = resolveGrokHookSessionId(env, harnessRoot, options);
157
+ if (grokSessionId) {
158
+ return `harness:${grokSessionId}`;
159
+ }
160
+ }
143
161
  if (harnessRoot) {
144
162
  return `pid:${harnessRoot.pid}@${harnessRoot.startTime}`;
145
163
  }
@@ -165,7 +183,7 @@ function resolveHarnessProcessRef(signal, processRef, inspector) {
165
183
  // process whose command matches the named harness. Anchoring session id to
166
184
  // that root keeps `tt` invocations stable whether they're spawned directly
167
185
  // by the harness (MCP subprocess) or through intermediate shells (CLI shell-out).
168
- function findHarnessRootInAncestry(harness, startPid, startInspection, inspector, maxDepth = 10) {
186
+ export function findHarnessRootInAncestry(harness, startPid, startInspection, inspector, maxDepth = 10) {
169
187
  let result = null;
170
188
  let currentPid = startPid;
171
189
  let currentInspection = startInspection;
@@ -220,6 +238,7 @@ const HARNESS_COMMAND_MAPPING = {
220
238
  "claude-code": "claude",
221
239
  codex: "codex",
222
240
  gemini: "gemini",
241
+ grok: "grok",
223
242
  opencode: "opencode"
224
243
  };
225
244
  function detectHarnessViaAncestry(pid, inspector, maxDepth = 10) {
@@ -231,10 +250,13 @@ function detectHarnessViaAncestry(pid, inspector, maxDepth = 10) {
231
250
  if (!inspection)
232
251
  break;
233
252
  const label = deriveCommandLabel(inspection.command);
234
- if (HARNESS_COMMAND_MAPPING[label]) {
253
+ const harness = HARNESS_COMMAND_MAPPING[label];
254
+ if (harness) {
235
255
  return {
236
- harness: HARNESS_COMMAND_MAPPING[label],
237
- sessionId: `pid:${inspection.pid}@${inspection.startTime}`,
256
+ harness,
257
+ sessionId: harness === "grok"
258
+ ? null
259
+ : `pid:${inspection.pid}@${inspection.startTime}`,
238
260
  pidHint: null
239
261
  };
240
262
  }
@@ -267,8 +289,51 @@ function detectHarnessSignal(env) {
267
289
  pidHint: null
268
290
  };
269
291
  }
292
+ const cmuxHarness = resolveCmuxLaunchHarness(env);
293
+ if (cmuxHarness) {
294
+ return {
295
+ harness: cmuxHarness,
296
+ sessionId: null,
297
+ pidHint: null
298
+ };
299
+ }
270
300
  return null;
271
301
  }
302
+ function detectGrokViaAncestry(parentPid, parentInspection, inspector) {
303
+ const grokRoot = findHarnessRootInAncestry("grok", parentPid, parentInspection, inspector, 20);
304
+ return grokRoot
305
+ ? { harness: "grok", sessionId: null, pidHint: null }
306
+ : null;
307
+ }
308
+ function resolveGrokHookSessionId(env, harnessRoot, options) {
309
+ const workspaceRoot = resolveGrokWorkspaceRoot(env, options.contextPath);
310
+ const record = findGrokSessionRecord({
311
+ logPath: options.grokSessionLogPath ??
312
+ resolveGrokSessionLogPath({ env }),
313
+ workspaceRoot,
314
+ grokPid: harnessRoot?.pid ?? null,
315
+ grokProcessStartedAt: harnessRoot?.startTime ?? null,
316
+ now: options.now
317
+ });
318
+ return record?.grok_session_id ?? null;
319
+ }
320
+ function resolveGrokWorkspaceRoot(env, contextPath) {
321
+ const explicit = nonEmpty(env.GROK_WORKSPACE_ROOT) ??
322
+ nonEmpty(env.CLAUDE_PROJECT_DIR);
323
+ if (explicit)
324
+ return path.resolve(explicit);
325
+ const candidate = contextPath ?? nonEmpty(env.PWD) ?? process.cwd();
326
+ try {
327
+ return resolveContextPath(candidate).workspace_root;
328
+ }
329
+ catch {
330
+ return path.resolve(candidate);
331
+ }
332
+ }
333
+ function resolveCmuxLaunchHarness(env) {
334
+ const launchKind = normalizeEnvValue(env.CMUX_AGENT_LAUNCH_KIND);
335
+ return launchKind ? HARNESS_COMMAND_MAPPING[launchKind] ?? null : null;
336
+ }
272
337
  function resolveSignalProcessRef(signal, fallbackPid, fallbackInspection, inspector) {
273
338
  if (signal.pidHint && signal.pidHint !== fallbackPid) {
274
339
  const hintedInspection = inspector.inspect(signal.pidHint);
@@ -294,6 +359,10 @@ function parsePositiveInteger(value) {
294
359
  function nonEmpty(value) {
295
360
  return value && value.trim().length > 0 ? value : null;
296
361
  }
362
+ function normalizeEnvValue(value) {
363
+ const nonBlank = nonEmpty(value);
364
+ return nonBlank ? nonBlank.toLowerCase() : null;
365
+ }
297
366
  function deriveCommandLabel(command) {
298
367
  if (!command) {
299
368
  return "harness";
package/dist/index.js CHANGED
@@ -5,7 +5,8 @@ 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
7
  export { DEFAULT_MAX_INSTRUCTION_FILE_BYTES, DEFAULT_INSTRUCTIONS_MARKDOWN, editInstructions, extractHarnessInstructions, normalizeInstructionHarness, parseInstructionScope, resetInstructions, resolveInstructionHarness, resolveInstructionPaths, showInstructions } from "./instructions.js";
8
- export { SUPPORTED_HARNESSES, MissingHarnessError, detectHarness, parseHarnessList, planUninstall, resolveHarnessConfigDir, resolveOpencodeConfigDir, resolveOpencodeConfigPath, runAction, skipAction } from "./install.js";
8
+ export { SUPPORTED_HARNESSES, buildGrokSessionHookConfig, DEFAULT_GROK_SESSION_HOOK_COMMAND, GROK_SESSION_HOOK_EVENTS, GROK_SESSION_HOOK_FILE, MissingHarnessError, detectHarness, parseHarnessList, planGrokSessionHookInstall, planGrokSessionHookUninstall, planUninstall, resolveGrokSessionHookPath, resolveHarnessConfigDir, resolveOpencodeConfigDir, resolveOpencodeConfigPath, runAction, skipAction } from "./install.js";
9
+ export { DEFAULT_GROK_SESSION_RECORD_MAX_AGE_MS, appendGrokSessionRecord, findGrokSessionRecord, isGrokSessionEndEvent, readGrokSessionRecords, resolveGrokSessionLogPath } from "./grok-session-store.js";
9
10
  export { DEFAULT_SKILL_NAME, planSkillInstall, planSkillUninstall, resolveBundledSkillPath, resolveSkillTargetPath, syncInstalledSkills } from "./skill-install.js";
10
11
  export { readPackageVersion, readUpdateMigrationState, resolveUpdateMigrationStatePath, runFirstRunMcpMigration, runStaleMcpCleanup, writeUpdateMigrationState } from "./update-migration.js";
11
12
  export { createSystemProcessInspector, terminateKnownProcess } from "./process-utils.js";
package/dist/install.js CHANGED
@@ -2,9 +2,17 @@ import { spawn } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
- export const SUPPORTED_HARNESSES = ["claude-code", "codex", "gemini", "opencode"];
5
+ export const SUPPORTED_HARNESSES = ["claude-code", "codex", "gemini", "grok", "opencode"];
6
6
  export const DEFAULT_SERVER_NAME = "talking-stick";
7
7
  export const DEFAULT_SERVER_COMMAND = ["tt", "mcp"];
8
+ export const GROK_SESSION_HOOK_FILE = "talking-stick-session.json";
9
+ export const DEFAULT_GROK_SESSION_HOOK_COMMAND = ": talking-stick-grok-session-hook; if command -v tt >/dev/null 2>&1; then tt grok-session-hook >/dev/null 2>/dev/null || true; fi";
10
+ export const GROK_SESSION_HOOK_EVENTS = [
11
+ "SessionStart",
12
+ "UserPromptSubmit",
13
+ "PreToolUse",
14
+ "SessionEnd"
15
+ ];
8
16
  export class MissingHarnessError extends Error {
9
17
  constructor(message) {
10
18
  super(message);
@@ -96,11 +104,21 @@ export function resolveHarnessConfigDir(harness, options = {}) {
96
104
  const resolved = resolveOptions(options);
97
105
  return resolveHarnessConfigDirFromResolved(harness, resolved);
98
106
  }
107
+ export function resolveGrokSessionHookPath(options = {}) {
108
+ const resolved = resolveOptions(options);
109
+ return path.join(resolveGrokConfigDirFromResolved(resolved), "hooks", GROK_SESSION_HOOK_FILE);
110
+ }
99
111
  function resolveOpencodeConfigDirFromResolved(resolved) {
100
112
  const xdg = resolved.env.XDG_CONFIG_HOME?.trim();
101
113
  const base = xdg && xdg.length > 0 ? xdg : path.join(resolved.homeDir, ".config");
102
114
  return path.join(base, "opencode");
103
115
  }
116
+ function resolveGrokConfigDirFromResolved(resolved) {
117
+ const grokHome = resolved.env.GROK_HOME?.trim();
118
+ return grokHome && grokHome.length > 0
119
+ ? grokHome
120
+ : path.join(resolved.homeDir, ".grok");
121
+ }
104
122
  function resolveHarnessConfigDirFromResolved(harness, resolved) {
105
123
  switch (harness) {
106
124
  case "claude-code":
@@ -109,6 +127,8 @@ function resolveHarnessConfigDirFromResolved(harness, resolved) {
109
127
  return path.join(resolved.homeDir, ".codex");
110
128
  case "gemini":
111
129
  return path.join(resolved.homeDir, ".gemini");
130
+ case "grok":
131
+ return resolveGrokConfigDirFromResolved(resolved);
112
132
  case "opencode":
113
133
  return resolveOpencodeConfigDirFromResolved(resolved);
114
134
  default:
@@ -157,6 +177,8 @@ export function planUninstall(harness, options = {}) {
157
177
  operation: "uninstall",
158
178
  serverName: resolved.serverName
159
179
  };
180
+ case "grok":
181
+ return skipAction(harness, "legacy Talking Stick cleanup is not applicable for grok");
160
182
  case "opencode": {
161
183
  const filePath = resolveOpencodeConfigPath(options);
162
184
  const configDir = path.dirname(filePath);
@@ -189,6 +211,76 @@ export function skipAction(harness, message) {
189
211
  message
190
212
  };
191
213
  }
214
+ export function planGrokSessionHookInstall(options = {}) {
215
+ const resolved = resolveOptions(options);
216
+ const grokConfigDir = resolveGrokConfigDirFromResolved(resolved);
217
+ const filePath = resolveGrokSessionHookPath(options);
218
+ if (resolved.skipMissing && !resolved.hooks.pathExists(grokConfigDir)) {
219
+ return skipAction("grok", `grok config directory not found: ${grokConfigDir}`);
220
+ }
221
+ return {
222
+ kind: "file-patch",
223
+ harness: "grok",
224
+ filePath,
225
+ description: `write Grok session hook ${filePath}`,
226
+ inspect: () => inspectGrokSessionHook(filePath, resolved),
227
+ apply: () => writeGrokSessionHook(filePath, resolved)
228
+ };
229
+ }
230
+ export function planGrokSessionHookUninstall(options = {}) {
231
+ const resolved = resolveOptions(options);
232
+ const grokConfigDir = resolveGrokConfigDirFromResolved(resolved);
233
+ const filePath = resolveGrokSessionHookPath(options);
234
+ if (resolved.skipMissing && !resolved.hooks.pathExists(grokConfigDir)) {
235
+ return skipAction("grok", `grok config directory not found: ${grokConfigDir}`);
236
+ }
237
+ return {
238
+ kind: "file-patch",
239
+ harness: "grok",
240
+ filePath,
241
+ description: `remove Grok session hook ${filePath}`,
242
+ inspect: () => resolved.hooks.readFile(filePath) === null ? "absent" : "present",
243
+ apply: () => removeGrokSessionHook(filePath, resolved)
244
+ };
245
+ }
246
+ export function buildGrokSessionHookConfig() {
247
+ const hook = {
248
+ type: "command",
249
+ command: DEFAULT_GROK_SESSION_HOOK_COMMAND,
250
+ timeout: 5
251
+ };
252
+ const hooks = Object.fromEntries(GROK_SESSION_HOOK_EVENTS.map((event) => [
253
+ event,
254
+ [
255
+ {
256
+ hooks: [hook]
257
+ }
258
+ ]
259
+ ]));
260
+ return JSON.stringify({ hooks }, null, 2) + "\n";
261
+ }
262
+ function inspectGrokSessionHook(filePath, resolved) {
263
+ const existing = resolved.hooks.readFile(filePath);
264
+ if (existing === null)
265
+ return "absent";
266
+ return existing === buildGrokSessionHookConfig() ? "present" : "different";
267
+ }
268
+ function writeGrokSessionHook(filePath, resolved) {
269
+ resolved.hooks.ensureDir(path.dirname(filePath));
270
+ resolved.hooks.writeFile(filePath, buildGrokSessionHookConfig());
271
+ }
272
+ function removeGrokSessionHook(filePath, resolved) {
273
+ void resolved;
274
+ try {
275
+ fs.rmSync(filePath, { force: true });
276
+ }
277
+ catch (error) {
278
+ if (error.code === "ENOENT") {
279
+ return;
280
+ }
281
+ throw error;
282
+ }
283
+ }
192
284
  function patchOpencodeConfig(filePath, resolved, mode) {
193
285
  const existing = resolved.hooks.readFile(filePath);
194
286
  if (resolved.skipMissing) {
@@ -305,6 +397,15 @@ export function detectHarness(harness, options = {}) {
305
397
  return { harness, detected: true, evidence: configDir };
306
398
  return { harness, detected: false, evidence: "gemini not on PATH and no config directory" };
307
399
  }
400
+ case "grok": {
401
+ const bin = resolved.hooks.which("grok");
402
+ if (bin)
403
+ return { harness, detected: true, evidence: bin };
404
+ const configDir = resolveHarnessConfigDirFromResolved(harness, resolved);
405
+ if (resolved.hooks.pathExists(configDir))
406
+ return { harness, detected: true, evidence: configDir };
407
+ return { harness, detected: false, evidence: "grok not on PATH and no config directory" };
408
+ }
308
409
  case "opencode": {
309
410
  const bin = resolved.hooks.which("opencode");
310
411
  if (bin)
@@ -534,6 +635,8 @@ function mcpConfigLocation(action) {
534
635
  return "Codex global config";
535
636
  case "gemini":
536
637
  return "Gemini user config";
638
+ case "grok":
639
+ return "Grok config";
537
640
  case "opencode":
538
641
  return "OpenCode config";
539
642
  default:
@@ -30,6 +30,10 @@ Lean into adversarial review, convergence, precise implementation, edge-case swe
30
30
 
31
31
  Use broad context review and exploration conservatively until the project has stronger Gemini-specific dogfood. Keep handoffs concrete and do not assume responsibility that the operator assigned to another harness.
32
32
 
33
+ ## Grok
34
+
35
+ Use Grok Build as a first-class local coding harness. Keep coordination safety ahead of speed, rely on the native Grok skill and session hook when installed, and keep handoffs concrete when another harness is better positioned to implement or review.
36
+
33
37
  ## OpenCode
34
38
 
35
39
  Use terminal-native local exploration and implementation conservatively until the project has stronger OpenCode-specific dogfood. Keep coordination safety ahead of speed.
@@ -41,6 +45,8 @@ const HARNESS_ALIASES = {
41
45
  "claude-code": "claude",
42
46
  codex: "codex",
43
47
  gemini: "gemini",
48
+ grok: "grok",
49
+ "grok-build": "grok",
44
50
  opencode: "opencode"
45
51
  };
46
52
  export function resolveInstructionPaths(options = {}) {
@@ -108,7 +114,7 @@ export function resolveInstructionHarness(explicitHarness, identity) {
108
114
  export function normalizeInstructionHarness(value) {
109
115
  const normalized = HARNESS_ALIASES[normalizeKey(value)];
110
116
  if (!normalized) {
111
- throw new Error(`--harness must be one of claude, codex, gemini, opencode, all (got ${value}).`);
117
+ throw new Error(`--harness must be one of claude, codex, gemini, grok, opencode, all (got ${value}).`);
112
118
  }
113
119
  return normalized;
114
120
  }
@@ -208,6 +214,8 @@ function parseHarnessHeader(line) {
208
214
  return "codex";
209
215
  if (key.startsWith("gemini"))
210
216
  return "gemini";
217
+ if (key.startsWith("grok"))
218
+ return "grok";
211
219
  if (key.startsWith("opencode"))
212
220
  return "opencode";
213
221
  return null;
@@ -1,5 +1,6 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
+ import os from "node:os";
3
4
  import path from "node:path";
4
5
  const workspaceMarkers = [
5
6
  "CLAUDE.md",
@@ -78,8 +79,12 @@ function resolveGitRoot(canonicalContextPath) {
78
79
  }
79
80
  }
80
81
  function findNearestWorkspaceMarker(startPath) {
82
+ const homeMarkerBoundary = resolveHomeMarkerBoundary(startPath);
81
83
  let current = startPath;
82
84
  while (true) {
85
+ if (homeMarkerBoundary && samePath(current, homeMarkerBoundary)) {
86
+ return null;
87
+ }
83
88
  for (const marker of workspaceMarkers) {
84
89
  if (fs.existsSync(path.join(current, marker))) {
85
90
  return current;
@@ -92,6 +97,32 @@ function findNearestWorkspaceMarker(startPath) {
92
97
  current = parent;
93
98
  }
94
99
  }
100
+ function resolveHomeMarkerBoundary(startPath) {
101
+ const homeDir = os.homedir();
102
+ if (!homeDir) {
103
+ return null;
104
+ }
105
+ const resolvedHomeDir = path.resolve(homeDir);
106
+ const candidateHomes = [
107
+ canonicalizeDirectoryPath(resolvedHomeDir),
108
+ path.normalize(resolvedHomeDir)
109
+ ];
110
+ for (const candidateHome of candidateHomes) {
111
+ if (!samePath(startPath, candidateHome) &&
112
+ isWithinOrSame(startPath, candidateHome)) {
113
+ return candidateHome;
114
+ }
115
+ }
116
+ return null;
117
+ }
118
+ function canonicalizeDirectoryPath(directoryPath) {
119
+ try {
120
+ return fs.realpathSync.native(directoryPath);
121
+ }
122
+ catch {
123
+ return path.normalize(directoryPath);
124
+ }
125
+ }
95
126
  function samePath(left, right) {
96
127
  return path.normalize(left) === path.normalize(right);
97
128
  }