gipity 1.0.429 → 1.1.2

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.
Files changed (45) hide show
  1. package/README.md +67 -29
  2. package/dist/agents/claude-code.js +48 -0
  3. package/dist/agents/codex.js +45 -0
  4. package/dist/agents/grok.js +50 -0
  5. package/dist/agents/index.js +23 -0
  6. package/dist/agents/types.js +18 -0
  7. package/dist/api.js +14 -4
  8. package/dist/capture/sources/codex.js +216 -0
  9. package/dist/capture/sources/grok.js +178 -0
  10. package/dist/catalog.js +46 -0
  11. package/dist/client-context.js +2 -0
  12. package/dist/commands/add.js +9 -22
  13. package/dist/commands/build.js +1330 -0
  14. package/dist/commands/chat.js +9 -3
  15. package/dist/commands/claude.js +4 -13
  16. package/dist/commands/db.js +22 -5
  17. package/dist/commands/deploy.js +7 -0
  18. package/dist/commands/doctor.js +8 -2
  19. package/dist/commands/fn.js +24 -1
  20. package/dist/commands/init.js +9 -15
  21. package/dist/commands/logs.js +21 -2
  22. package/dist/commands/page-eval.js +25 -10
  23. package/dist/commands/page-inspect.js +7 -3
  24. package/dist/commands/page-screenshot.js +62 -23
  25. package/dist/commands/project.js +1 -1
  26. package/dist/commands/relay-install.js +1 -1
  27. package/dist/commands/relay.js +3 -3
  28. package/dist/commands/sandbox.js +62 -16
  29. package/dist/commands/setup.js +16 -10
  30. package/dist/commands/status.js +35 -7
  31. package/dist/commands/test.js +7 -0
  32. package/dist/commands/uninstall.js +25 -3
  33. package/dist/flag-aliases.js +1 -0
  34. package/dist/hooks/capture-runner.js +127 -38
  35. package/dist/index.js +1118 -361
  36. package/dist/knowledge.js +3 -3
  37. package/dist/prefs.js +42 -0
  38. package/dist/project-setup.js +2 -10
  39. package/dist/relay/daemon.js +124 -16
  40. package/dist/relay/diagnostics.js +4 -2
  41. package/dist/relay/onboarding.js +9 -9
  42. package/dist/relay/setup.js +8 -8
  43. package/dist/relay/state.js +1 -1
  44. package/dist/setup.js +258 -17
  45. package/package.json +4 -3
@@ -1,31 +1,36 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Internal hook runner - invoked by Claude Code's lifecycle hooks
3
+ * Internal hook runner - invoked by the coding agent's lifecycle hooks
4
4
  * (SessionStart, Stop, SubagentStop, SessionEnd, and a throttled
5
- * PostToolUse for mid-run flushing) to mirror a terminal Claude Code
6
- * session into the Gipity server so the web CLI can display it read-only.
5
+ * PostToolUse for mid-run flushing) to mirror a terminal agent session
6
+ * into the Gipity server so the web CLI can display it read-only.
7
+ *
8
+ * Works for every supported agent: Claude Code and Grok Build fire it
9
+ * through the Gipity plugin's hooks (Grok runs Claude-format plugin hooks
10
+ * natively; capture.cjs rewrites the source to 'grok' when it sees
11
+ * GROK_HOOK_EVENT), Codex through the project's .codex/hooks.json. Each
12
+ * source has its own transcript parser under cli/src/capture/sources/.
7
13
  *
8
14
  * Not a user-facing `gipity` subcommand by design: users never invoke
9
- * this directly. The Gipity Claude Code plugin's capture hook script
10
- * (skills repo hooks/scripts/capture.cjs) resolves this file inside the
11
- * installed CLI at fire time and runs it - so the capture logic versions
12
- * with the CLI, not the plugin.
15
+ * this directly. The hook scripts (skills repo hooks/scripts/capture.cjs)
16
+ * resolve this file inside the installed CLI at fire time and run it - so
17
+ * the capture logic versions with the CLI, not the plugin.
13
18
  *
14
19
  * Usage:
15
20
  * node capture-runner.js <source> <event>
16
- * source: 'claude-code' (today) | future: 'codex',
21
+ * source: 'claude-code' | 'codex' | 'grok'
17
22
  * event: 'session-start' | 'stop' | 'subagent-stop' | 'session-end' | 'post-tool-use' | 'pre-compact'
18
23
  *
19
24
  * Conversation binding, in order:
20
- * 1. GIPITY_CONVERSATION_GUID env var - set by `gipity claude`, which
21
- * created/reused the conversation before spawning Claude Code.
25
+ * 1. GIPITY_CONVERSATION_GUID env var - set by `gipity build`, which
26
+ * created/reused the conversation before spawning the agent.
22
27
  * 2. A session_id → conv mapping persisted in the capture-state dir by
23
28
  * an earlier event of this session.
24
- * 3. Self-arm: the session was launched WITHOUT `gipity claude` (bare
25
- * `claude` in a linked project dir). Resolve the project from
26
- * .gipity.json and ask the server to bind this session_id to a
27
- * conversation (POST /remote-sessions/resolve), then persist the
28
- * mapping. This is what makes bare `claude` sessions record.
29
+ * 3. Self-arm: the session was launched WITHOUT `gipity build` (bare
30
+ * `claude` / `codex` / `grok` in a linked project dir). Resolve the
31
+ * project from .gipity.json and ask the server to bind this
32
+ * session_id to a conversation (POST /remote-sessions/resolve), then
33
+ * persist the mapping. This is what makes bare agent sessions record.
29
34
  *
30
35
  * Graceful no-ops (exit 0 silently):
31
36
  * - GIPITY_CAPTURE=off - the relay daemon owns capture for this run
@@ -36,16 +41,43 @@
36
41
  * - Anything unexpected (parse error, network error, etc.). We must
37
42
  * not break the user's interactive session.
38
43
  */
39
- import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, openSync, closeSync, statSync, utimesSync, createReadStream, } from 'fs';
44
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, unlinkSync, openSync, closeSync, statSync, utimesSync, createReadStream, } from 'fs';
40
45
  import { homedir } from 'os';
41
46
  import { join } from 'path';
42
47
  import { getDevice } from '../relay/state.js';
43
48
  import { deviceFetch } from '../relay/device-http.js';
44
49
  import { ImageBlockRewriter } from '../relay/media-upload.js';
45
50
  import { getConfig } from '../config.js';
46
- import { parseTranscript, } from '../capture/sources/claude-code.js';
51
+ import { parseTranscript as parseClaudeTranscript, } from '../capture/sources/claude-code.js';
52
+ import { parseTranscript as parseCodexTranscript } from '../capture/sources/codex.js';
53
+ import { parseTranscript as parseGrokTranscript } from '../capture/sources/grok.js';
47
54
  const CAPTURE_DIR = join(homedir(), '.gipity', 'capture-state');
48
55
  const INGEST_BATCH_MAX = 100; // server caps at 200; stay comfortably under
56
+ export const CAPTURE_SOURCES = {
57
+ 'claude-code': {
58
+ serverSource: 'claude_code',
59
+ displayName: 'Claude Code',
60
+ parse: (content, afterUuid) => parseClaudeTranscript(content, afterUuid),
61
+ },
62
+ codex: {
63
+ serverSource: 'codex',
64
+ displayName: 'Codex',
65
+ // Codex hook payloads always carry transcript_path (the rollout file).
66
+ parse: (content, afterUuid) => parseCodexTranscript(content, afterUuid),
67
+ },
68
+ grok: {
69
+ serverSource: 'grok',
70
+ displayName: 'Grok',
71
+ parse: (content, afterUuid, hook) => parseGrokTranscript(content, afterUuid, { sessionId: hook.session_id }),
72
+ // Grok's session dir is derivable: ~/.grok/sessions/<urlencoded-cwd>/<sid>/
73
+ resolveTranscriptPath: (hook) => {
74
+ if (!hook.session_id)
75
+ return null;
76
+ const cwd = hook.cwd ?? process.cwd();
77
+ return join(homedir(), '.grok', 'sessions', encodeURIComponent(cwd), hook.session_id, 'chat_history.jsonl');
78
+ },
79
+ },
80
+ };
49
81
  // PostToolUse fires after every tool call. We flush on it so a session that is
50
82
  // killed/crashes mid-run (e.g. a long headless `gipity claude -p` build that
51
83
  // hits a timeout) still has its transcript in the DB - Stop/SessionEnd only
@@ -212,13 +244,14 @@ function deleteSessionMap(sessionId) {
212
244
  * already bound to this session_id (resume), the project's still-empty
213
245
  * placeholder, or a fresh one. Returns null on any failure - capture is
214
246
  * best-effort and must never break the session. */
215
- async function resolveFromServer(projectGuid, hook) {
247
+ async function resolveFromServer(projectGuid, hook, serverSource) {
216
248
  let res;
217
249
  try {
218
250
  res = await deviceFetch('POST', '/remote-sessions/resolve', {
219
251
  project_guid: projectGuid,
220
252
  session_id: hook.session_id,
221
253
  cwd: hook.cwd,
254
+ source: serverSource,
222
255
  }, 15_000);
223
256
  }
224
257
  catch {
@@ -240,7 +273,7 @@ async function resolveFromServer(projectGuid, hook) {
240
273
  * the same crash-safe lock the flushers use, so concurrent hooks (Stop +
241
274
  * SubagentStop) can't each mint a conversation: the loser polls for the
242
275
  * winner's mapping instead of racing the server. */
243
- export async function resolveConvGuid(hook, sleep = (ms) => new Promise(r => setTimeout(r, ms))) {
276
+ export async function resolveConvGuid(hook, serverSource = 'claude_code', sleep = (ms) => new Promise(r => setTimeout(r, ms))) {
244
277
  const fromEnv = process.env.GIPITY_CONVERSATION_GUID;
245
278
  if (fromEnv)
246
279
  return fromEnv;
@@ -276,7 +309,7 @@ export async function resolveConvGuid(hook, sleep = (ms) => new Promise(r => set
276
309
  const raced = readSessionMap(sessionId);
277
310
  if (raced)
278
311
  return raced;
279
- const conv = await resolveFromServer(config.projectGuid, hook);
312
+ const conv = await resolveFromServer(config.projectGuid, hook, serverSource);
280
313
  if (conv)
281
314
  writeSessionMap(sessionId, conv);
282
315
  return conv;
@@ -338,7 +371,7 @@ async function handleSessionStart(convGuid, hook) {
338
371
  }];
339
372
  await postEntries(convGuid, entries);
340
373
  }
341
- async function handleStopFamily(convGuid, hook, isSubagent, minIntervalMs = 0) {
374
+ async function handleStopFamily(convGuid, src, hook, isSubagent, minIntervalMs = 0) {
342
375
  void isSubagent;
343
376
  if (!hook.transcript_path || !existsSync(hook.transcript_path))
344
377
  return;
@@ -355,11 +388,11 @@ async function handleStopFamily(convGuid, hook, isSubagent, minIntervalMs = 0) {
355
388
  try {
356
389
  const state = readState(convGuid) ?? { last_uuid: null };
357
390
  const content = await readWholeFile(hook.transcript_path);
358
- let result = parseTranscript(content, state.last_uuid);
391
+ let result = src.parse(content, state.last_uuid, hook);
359
392
  if (!result.foundWatermark && state.last_uuid !== null) {
360
393
  // Transcript rotated (/clear or compact) - watermark isn't present.
361
394
  // Replay from top; server dedupes via the source_uuid unique index.
362
- result = parseTranscript(content, null);
395
+ result = src.parse(content, null, hook);
363
396
  }
364
397
  // Base64 image blocks (Read-of-image tool results) upload to VFS and
365
398
  // become image_ref blocks — same no-inline-base64 chokepoint as the
@@ -379,18 +412,17 @@ async function handleStopFamily(convGuid, hook, isSubagent, minIntervalMs = 0) {
379
412
  release();
380
413
  }
381
414
  }
382
- async function handleSessionEnd(convGuid, hook, source) {
415
+ async function handleSessionEnd(convGuid, src, hook) {
383
416
  // Flush any tail lines one last time - SessionEnd fires after the final
384
417
  // Stop, so there's usually nothing new, but a race between Stop and
385
418
  // SessionEnd could leave lines behind.
386
419
  if (hook.transcript_path && existsSync(hook.transcript_path)) {
387
- await handleStopFamily(convGuid, hook, false);
420
+ await handleStopFamily(convGuid, src, hook, false);
388
421
  }
389
422
  const sessionId = hook.session_id ?? 'unknown';
390
- const finishedLabel = displayName(source);
391
423
  const entries = [{
392
424
  kind: 'system',
393
- content: `${finishedLabel} finished`,
425
+ content: `${src.displayName} finished`,
394
426
  source_uuid: `${sessionId}-end`,
395
427
  }];
396
428
  await postEntries(convGuid, entries);
@@ -398,10 +430,54 @@ async function handleSessionEnd(convGuid, hook, source) {
398
430
  if (hook.session_id)
399
431
  deleteSessionMap(hook.session_id);
400
432
  }
401
- function displayName(source) {
402
- if (source === 'claude-code')
403
- return 'Claude Code';
404
- return source;
433
+ // Codex has no SessionEnd hook, so its capture-state/session-map files are
434
+ // never cleaned by handleSessionEnd. Sweep anything stale on the way through
435
+ // - the files are tiny, so a generous TTL is fine; a live session's state is
436
+ // rewritten on every flush and never gets this old.
437
+ const STATE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
438
+ function sweepStaleState() {
439
+ let names;
440
+ try {
441
+ names = readdirSync(CAPTURE_DIR);
442
+ }
443
+ catch {
444
+ return;
445
+ }
446
+ const cutoff = Date.now() - STATE_TTL_MS;
447
+ for (const name of names) {
448
+ const p = join(CAPTURE_DIR, name);
449
+ try {
450
+ if (statSync(p).mtimeMs < cutoff)
451
+ unlinkSync(p);
452
+ }
453
+ catch { /* raced or unreadable - leave it */ }
454
+ }
455
+ }
456
+ /** Normalize a raw hook payload to snake_case HookInput. Claude Code and
457
+ * Codex deliver snake_case (`session_id`, `transcript_path`, `cwd`); Grok
458
+ * Build delivers camelCase (`sessionId`, `hookEventName`, …). Accept both
459
+ * so one runner serves every harness. */
460
+ export function normalizeHookInput(raw) {
461
+ if (!raw || typeof raw !== 'object')
462
+ return {};
463
+ const pick = (...keys) => {
464
+ for (const k of keys) {
465
+ const v = raw[k];
466
+ if (typeof v === 'string' && v)
467
+ return v;
468
+ }
469
+ return undefined;
470
+ };
471
+ const hook = {
472
+ session_id: pick('session_id', 'sessionId'),
473
+ transcript_path: pick('transcript_path', 'transcriptPath'),
474
+ cwd: pick('cwd', 'workingDirectory'),
475
+ hook_event_name: pick('hook_event_name', 'hookEventName'),
476
+ };
477
+ // Preserve extras the handlers peek at (e.g. pre-compact's `trigger`).
478
+ if (typeof raw.trigger === 'string')
479
+ hook.trigger = raw.trigger;
480
+ return hook;
405
481
  }
406
482
  async function main() {
407
483
  // Explicit capture opt-out. Set by the relay daemon's dispatch spawns
@@ -415,15 +491,28 @@ async function main() {
415
491
  const [source, event] = process.argv.slice(2);
416
492
  if (!source || !event)
417
493
  return;
494
+ const src = CAPTURE_SOURCES[source];
495
+ if (!src)
496
+ return; // unknown agent - silent no-op
418
497
  const stdin = await readStdin();
419
498
  let hook = {};
420
499
  if (stdin.trim()) {
421
500
  try {
422
- hook = JSON.parse(stdin);
501
+ hook = normalizeHookInput(JSON.parse(stdin));
423
502
  }
424
503
  catch { /* ignore - event may not require transcript */ }
425
504
  }
426
- const convGuid = await resolveConvGuid(hook);
505
+ // Agents whose hook payloads omit the transcript path (Grok) get it
506
+ // derived from the session id + cwd.
507
+ if (!hook.transcript_path && src.resolveTranscriptPath) {
508
+ const derived = src.resolveTranscriptPath(hook);
509
+ if (derived)
510
+ hook.transcript_path = derived;
511
+ }
512
+ // Opportunistic hygiene: Codex never fires session-end, so its state
513
+ // files are TTL-swept instead of deleted at end-of-session.
514
+ sweepStaleState();
515
+ const convGuid = await resolveConvGuid(hook, src.serverSource);
427
516
  if (!convGuid)
428
517
  return; // not a Gipity-bound session - nothing to capture
429
518
  try {
@@ -432,24 +521,24 @@ async function main() {
432
521
  await handleSessionStart(convGuid, hook);
433
522
  break;
434
523
  case 'stop':
435
- await handleStopFamily(convGuid, hook, false);
524
+ await handleStopFamily(convGuid, src, hook, false);
436
525
  break;
437
526
  case 'subagent-stop':
438
- await handleStopFamily(convGuid, hook, true);
527
+ await handleStopFamily(convGuid, src, hook, true);
439
528
  break;
440
529
  case 'post-tool-use':
441
530
  // Incremental mid-run flush so an interrupted session keeps its
442
531
  // transcript (Stop/SessionEnd only fire on clean exit). Throttled.
443
- await handleStopFamily(convGuid, hook, false, POST_TOOL_FLUSH_MS);
532
+ await handleStopFamily(convGuid, src, hook, false, POST_TOOL_FLUSH_MS);
444
533
  break;
445
534
  case 'session-end':
446
- await handleSessionEnd(convGuid, hook, source);
535
+ await handleSessionEnd(convGuid, src, hook);
447
536
  break;
448
537
  case 'pre-compact': {
449
538
  // Flush the transcript tail BEFORE compaction rewrites it (the
450
539
  // watermark replay after a rewrite relies on server dedup, but
451
540
  // flushing first keeps ordering clean), then record the boundary.
452
- await handleStopFamily(convGuid, hook, false);
541
+ await handleStopFamily(convGuid, src, hook, false);
453
542
  const trigger = typeof hook.trigger === 'string' ? hook.trigger : 'auto';
454
543
  await postEntries(convGuid, [{
455
544
  kind: 'compact',