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.
- package/README.md +67 -29
- package/dist/agents/claude-code.js +48 -0
- package/dist/agents/codex.js +45 -0
- package/dist/agents/grok.js +50 -0
- package/dist/agents/index.js +23 -0
- package/dist/agents/types.js +18 -0
- package/dist/api.js +14 -4
- package/dist/capture/sources/codex.js +216 -0
- package/dist/capture/sources/grok.js +178 -0
- package/dist/catalog.js +46 -0
- package/dist/client-context.js +2 -0
- package/dist/commands/add.js +9 -22
- package/dist/commands/build.js +1330 -0
- package/dist/commands/chat.js +9 -3
- package/dist/commands/claude.js +4 -13
- package/dist/commands/db.js +22 -5
- package/dist/commands/deploy.js +7 -0
- package/dist/commands/doctor.js +8 -2
- package/dist/commands/fn.js +24 -1
- package/dist/commands/init.js +9 -15
- package/dist/commands/logs.js +21 -2
- package/dist/commands/page-eval.js +25 -10
- package/dist/commands/page-inspect.js +7 -3
- package/dist/commands/page-screenshot.js +62 -23
- package/dist/commands/project.js +1 -1
- package/dist/commands/relay-install.js +1 -1
- package/dist/commands/relay.js +3 -3
- package/dist/commands/sandbox.js +62 -16
- package/dist/commands/setup.js +16 -10
- package/dist/commands/status.js +35 -7
- package/dist/commands/test.js +7 -0
- package/dist/commands/uninstall.js +25 -3
- package/dist/flag-aliases.js +1 -0
- package/dist/hooks/capture-runner.js +127 -38
- package/dist/index.js +1118 -361
- package/dist/knowledge.js +3 -3
- package/dist/prefs.js +42 -0
- package/dist/project-setup.js +2 -10
- package/dist/relay/daemon.js +124 -16
- package/dist/relay/diagnostics.js +4 -2
- package/dist/relay/onboarding.js +9 -9
- package/dist/relay/setup.js +8 -8
- package/dist/relay/state.js +1 -1
- package/dist/setup.js +258 -17
- package/package.json +4 -3
|
@@ -1,31 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Internal hook runner - invoked by
|
|
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
|
|
6
|
-
*
|
|
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
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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'
|
|
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
|
|
21
|
-
* created/reused the conversation before spawning
|
|
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
|
|
25
|
-
* `claude` in a linked project dir). Resolve the
|
|
26
|
-
* .gipity.json and ask the server to bind this
|
|
27
|
-
* conversation (POST /remote-sessions/resolve), then
|
|
28
|
-
* mapping. This is what makes bare
|
|
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 =
|
|
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 =
|
|
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,
|
|
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: `${
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
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,
|
|
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',
|