gipity 1.0.428 → 1.1.1
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/agents/claude-code.js +53 -0
- package/dist/agents/codex.js +49 -0
- package/dist/agents/grok.js +54 -0
- package/dist/agents/index.js +23 -0
- package/dist/agents/types.js +18 -0
- package/dist/api.js +7 -0
- package/dist/capture/sources/codex.js +216 -0
- package/dist/capture/sources/grok.js +178 -0
- package/dist/client-context.js +2 -0
- package/dist/commands/build.js +1352 -0
- package/dist/commands/chat.js +9 -3
- package/dist/commands/claude.js +4 -13
- package/dist/commands/db.js +12 -5
- package/dist/commands/deploy.js +19 -1
- package/dist/commands/doctor.js +8 -2
- package/dist/commands/generate.js +61 -4
- package/dist/commands/init.js +9 -15
- package/dist/commands/page-eval.js +404 -56
- package/dist/commands/page-inspect.js +15 -5
- package/dist/commands/page-screenshot.js +269 -64
- package/dist/commands/project.js +1 -1
- package/dist/commands/relay-install.js +1 -1
- package/dist/commands/relay.js +2 -2
- package/dist/commands/sandbox.js +62 -16
- package/dist/commands/setup.js +16 -10
- package/dist/commands/uninstall.js +25 -3
- package/dist/hooks/capture-runner.js +136 -39
- package/dist/index.js +1962 -668
- package/dist/knowledge.js +3 -3
- package/dist/page-fixtures.js +92 -3
- package/dist/prefs.js +50 -0
- package/dist/project-setup.js +2 -10
- package/dist/provider-docs.js +10 -10
- package/dist/relay/daemon.js +140 -18
- package/dist/relay/device-http.js +22 -0
- package/dist/relay/diagnostics.js +4 -2
- package/dist/relay/media-upload.js +184 -0
- package/dist/relay/onboarding.js +2 -2
- package/dist/setup.js +262 -18
- package/package.json +4 -3
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ensureClaudeInstalled, CLAUDE_PACKAGE } from '../claude-setup.js';
|
|
2
|
+
export const claudeCodeAdapter = {
|
|
3
|
+
key: 'claude',
|
|
4
|
+
source: 'claude_code',
|
|
5
|
+
displayName: 'Claude Code',
|
|
6
|
+
providerName: 'Anthropic',
|
|
7
|
+
binary: 'claude',
|
|
8
|
+
models: [
|
|
9
|
+
{ id: 'opus', label: 'Opus' },
|
|
10
|
+
{ id: 'sonnet', label: 'Sonnet' },
|
|
11
|
+
{ id: 'haiku', label: 'Haiku' },
|
|
12
|
+
],
|
|
13
|
+
buildInteractiveArgs({ resume, model }) {
|
|
14
|
+
const args = [];
|
|
15
|
+
if (model)
|
|
16
|
+
args.push('--model', model);
|
|
17
|
+
if (resume)
|
|
18
|
+
args.push('--resume', resume);
|
|
19
|
+
return args;
|
|
20
|
+
},
|
|
21
|
+
buildHeadlessArgs({ message, resume, model, bypassApprovals, jsonStream }) {
|
|
22
|
+
const args = ['-p', message];
|
|
23
|
+
if (bypassApprovals)
|
|
24
|
+
args.push('--permission-mode', 'bypassPermissions');
|
|
25
|
+
if (model)
|
|
26
|
+
args.push('--model', model);
|
|
27
|
+
if (resume)
|
|
28
|
+
args.push('--resume', resume);
|
|
29
|
+
// `--verbose` is required by Claude Code when combining -p with
|
|
30
|
+
// --output-format stream-json; partial messages feed live typing.
|
|
31
|
+
if (jsonStream)
|
|
32
|
+
args.push('--output-format', 'stream-json', '--verbose', '--include-partial-messages');
|
|
33
|
+
return args;
|
|
34
|
+
},
|
|
35
|
+
sessionIdFromStreamEvent(event) {
|
|
36
|
+
// Claude's stream-json: the system/init event (and the result footer)
|
|
37
|
+
// carry `session_id`.
|
|
38
|
+
if (event && typeof event.session_id === 'string' && event.session_id)
|
|
39
|
+
return event.session_id;
|
|
40
|
+
return null;
|
|
41
|
+
},
|
|
42
|
+
hooksSupportedOnPlatform() {
|
|
43
|
+
return true;
|
|
44
|
+
},
|
|
45
|
+
// The daemon parses Claude's stream-json into ingest entries itself
|
|
46
|
+
// (hook capture stands down via GIPITY_CAPTURE=off on dispatches).
|
|
47
|
+
daemonStreamCapture: true,
|
|
48
|
+
ensureInstalled() {
|
|
49
|
+
return ensureClaudeInstalled().installed;
|
|
50
|
+
},
|
|
51
|
+
installHint: `npm install -g ${CLAUDE_PACKAGE}`,
|
|
52
|
+
};
|
|
53
|
+
//# sourceMappingURL=claude-code.js.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export const codexAdapter = {
|
|
2
|
+
key: 'codex',
|
|
3
|
+
source: 'codex',
|
|
4
|
+
displayName: 'Codex',
|
|
5
|
+
providerName: 'OpenAI',
|
|
6
|
+
binary: 'codex',
|
|
7
|
+
models: [
|
|
8
|
+
{ id: 'gpt-5.1-codex', label: 'GPT-5.1 Codex' },
|
|
9
|
+
{ id: 'gpt-5.1-codex-mini', label: 'GPT-5.1 Codex Mini' },
|
|
10
|
+
],
|
|
11
|
+
buildInteractiveArgs({ resume, model }) {
|
|
12
|
+
const args = [];
|
|
13
|
+
if (model)
|
|
14
|
+
args.push('--model', model);
|
|
15
|
+
if (resume)
|
|
16
|
+
args.push('resume', resume);
|
|
17
|
+
return args;
|
|
18
|
+
},
|
|
19
|
+
buildHeadlessArgs({ message, resume, model, bypassApprovals, jsonStream }) {
|
|
20
|
+
// `codex exec resume <sid> "<msg>"` continues a session; plain
|
|
21
|
+
// `codex exec "<msg>"` starts fresh. Flags are shared by both forms.
|
|
22
|
+
const args = resume ? ['exec', 'resume', resume, message] : ['exec', message];
|
|
23
|
+
if (model)
|
|
24
|
+
args.push('--model', model);
|
|
25
|
+
if (bypassApprovals) {
|
|
26
|
+
args.push('-s', 'workspace-write', '-c', 'sandbox_workspace_write.network_access=true', '--dangerously-bypass-hook-trust', '--skip-git-repo-check');
|
|
27
|
+
}
|
|
28
|
+
if (jsonStream)
|
|
29
|
+
args.push('--json');
|
|
30
|
+
return args;
|
|
31
|
+
},
|
|
32
|
+
sessionIdFromStreamEvent(event) {
|
|
33
|
+
// `codex exec --json`: the thread.started event carries thread_id.
|
|
34
|
+
if (event?.type === 'thread.started' && typeof event.thread_id === 'string') {
|
|
35
|
+
return event.thread_id;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
},
|
|
39
|
+
hooksSupportedOnPlatform(platform) {
|
|
40
|
+
return platform !== 'win32'; // Codex hooks are disabled on Windows
|
|
41
|
+
},
|
|
42
|
+
// No stream-json ingest mapper for Codex: relay dispatches keep hook
|
|
43
|
+
// capture ON (GIPITY_CONVERSATION_GUID binds it) and the transcript
|
|
44
|
+
// parser mirrors the session; the daemon tracks byte-level progress only.
|
|
45
|
+
daemonStreamCapture: false,
|
|
46
|
+
oneTimeSetupNote: 'Codex runs project hooks only after a one-time approval: run /hooks inside Codex and approve the Gipity entries, or session recording stays off.',
|
|
47
|
+
installHint: 'npm install -g @openai/codex',
|
|
48
|
+
};
|
|
49
|
+
//# sourceMappingURL=codex.js.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export const grokAdapter = {
|
|
2
|
+
key: 'grok',
|
|
3
|
+
source: 'grok',
|
|
4
|
+
displayName: 'Grok',
|
|
5
|
+
providerName: 'xAI',
|
|
6
|
+
binary: 'grok',
|
|
7
|
+
models: [
|
|
8
|
+
{ id: 'grok-4.5', label: 'Grok 4.5' },
|
|
9
|
+
{ id: 'grok-code', label: 'Grok Code' },
|
|
10
|
+
],
|
|
11
|
+
buildInteractiveArgs({ resume, model }) {
|
|
12
|
+
const args = [];
|
|
13
|
+
if (model)
|
|
14
|
+
args.push('--model', model);
|
|
15
|
+
if (resume)
|
|
16
|
+
args.push('--resume', resume);
|
|
17
|
+
return args;
|
|
18
|
+
},
|
|
19
|
+
buildHeadlessArgs({ message, resume, model, bypassApprovals, jsonStream }) {
|
|
20
|
+
const args = ['-p', message];
|
|
21
|
+
if (model)
|
|
22
|
+
args.push('--model', model);
|
|
23
|
+
if (resume)
|
|
24
|
+
args.push('--resume', resume);
|
|
25
|
+
if (bypassApprovals)
|
|
26
|
+
args.push('--always-approve');
|
|
27
|
+
if (jsonStream)
|
|
28
|
+
args.push('--output-format', 'streaming-json');
|
|
29
|
+
return args;
|
|
30
|
+
},
|
|
31
|
+
sessionIdFromStreamEvent(event) {
|
|
32
|
+
// Grok's streaming-json is ACP-shaped: session/update notifications
|
|
33
|
+
// carry params.sessionId. Accept a top-level sessionId too, defensively.
|
|
34
|
+
const fromParams = event?.params?.sessionId;
|
|
35
|
+
if (typeof fromParams === 'string' && fromParams)
|
|
36
|
+
return fromParams;
|
|
37
|
+
if (typeof event?.sessionId === 'string' && event.sessionId)
|
|
38
|
+
return event.sessionId;
|
|
39
|
+
return null;
|
|
40
|
+
},
|
|
41
|
+
hooksSupportedOnPlatform() {
|
|
42
|
+
return true; // in the interactive TUI; headless runs use headlessCapture
|
|
43
|
+
},
|
|
44
|
+
daemonStreamCapture: false,
|
|
45
|
+
// Verified live (2026-07-13): grok -p fires NO plugin hooks - not even
|
|
46
|
+
// SessionStart/Stop - so headless capture is launcher-driven: pin the
|
|
47
|
+
// session id, then replay ~/.grok/sessions/<cwd>/<sid>/chat_history.jsonl
|
|
48
|
+
// through the capture runner after the run.
|
|
49
|
+
headlessCapture: {
|
|
50
|
+
sessionIdArgs: (sessionId) => ['--session-id', sessionId],
|
|
51
|
+
},
|
|
52
|
+
installHint: 'curl -fsSL https://grok.x.ai/install.sh | sh (or see docs.x.ai/grok-build)',
|
|
53
|
+
};
|
|
54
|
+
//# sourceMappingURL=grok.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { claudeCodeAdapter } from './claude-code.js';
|
|
2
|
+
import { codexAdapter } from './codex.js';
|
|
3
|
+
import { grokAdapter } from './grok.js';
|
|
4
|
+
/** Picker/help order: Claude first (the default), then the others. */
|
|
5
|
+
export const AGENT_ADAPTERS = [
|
|
6
|
+
claudeCodeAdapter,
|
|
7
|
+
codexAdapter,
|
|
8
|
+
grokAdapter,
|
|
9
|
+
];
|
|
10
|
+
export const AGENT_KEYS = AGENT_ADAPTERS.map(a => a.key);
|
|
11
|
+
export function getAdapter(key) {
|
|
12
|
+
const found = AGENT_ADAPTERS.find(a => a.key === key);
|
|
13
|
+
if (!found)
|
|
14
|
+
throw new Error(`Unknown agent "${key}". Valid: ${AGENT_KEYS.join(', ')}`);
|
|
15
|
+
return found;
|
|
16
|
+
}
|
|
17
|
+
export function getAdapterBySource(source) {
|
|
18
|
+
const found = AGENT_ADAPTERS.find(a => a.source === source);
|
|
19
|
+
if (!found)
|
|
20
|
+
throw new Error(`No agent adapter for source "${source}"`);
|
|
21
|
+
return found;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RemoteAgentAdapter - the one abstraction between Gipity and a local coding
|
|
3
|
+
* agent's EXECUTION surface (argv shapes, flags, stream schema). Everything
|
|
4
|
+
* data-side is already agent-agnostic: conversations carry a `source`
|
|
5
|
+
* discriminator, capture has a per-source parser registry
|
|
6
|
+
* (cli/src/hooks/capture-runner.ts CAPTURE_SOURCES), and the DAL/routes are
|
|
7
|
+
* parameterized. The adapter covers what a `source` string alone can't:
|
|
8
|
+
*
|
|
9
|
+
* - how to launch the agent binary (interactive and headless)
|
|
10
|
+
* - how to resume a session, pick a model, bypass approvals
|
|
11
|
+
* - how to spot the session id in the agent's output stream
|
|
12
|
+
*
|
|
13
|
+
* Two consumers: the `gipity build` launcher (interactive + `-p`) and the
|
|
14
|
+
* relay daemon (web-CLI dispatch). Adding an agent = one file here + one
|
|
15
|
+
* capture parser + one REMOTE_TYPES value; no scattered `if (source === …)`.
|
|
16
|
+
*/
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=types.js.map
|
package/dist/api.js
CHANGED
|
@@ -195,6 +195,13 @@ export async function sendMessage(message) {
|
|
|
195
195
|
if (res.data.conversationGuid !== config.conversationGuid) {
|
|
196
196
|
saveConfig({ ...config, conversationGuid: res.data.conversationGuid });
|
|
197
197
|
}
|
|
198
|
+
// The server's cost-consent gate stops the agentic loop on big-build
|
|
199
|
+
// requests and returns an explicit costWarning with empty content. Surface
|
|
200
|
+
// it - before this the CLI printed nothing and the command silently did
|
|
201
|
+
// nothing. Replying "y" (next sendMessage) consents and proceeds.
|
|
202
|
+
if (res.data.costWarning && !res.data.content) {
|
|
203
|
+
return res.data.costWarning.message;
|
|
204
|
+
}
|
|
198
205
|
return res.data.content;
|
|
199
206
|
}
|
|
200
207
|
/** Download a file as raw bytes (no JSON parsing) */
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex rollout JSONL transcript → ingest entries.
|
|
3
|
+
*
|
|
4
|
+
* Codex writes one JSON object per line to
|
|
5
|
+
* ~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<session_id>.jsonl
|
|
6
|
+
* and every hook payload carries `transcript_path` pointing at that file.
|
|
7
|
+
*
|
|
8
|
+
* IMPORTANT: the rollout format is NOT the `codex exec --json` stream schema
|
|
9
|
+
* (no `thread.started` / `item.*` events). Verified against real files
|
|
10
|
+
* (Codex CLI v0.144.1, 2026-07-13); each line is an envelope:
|
|
11
|
+
*
|
|
12
|
+
* { timestamp: ISO, type: <envelope>, payload: {...} }
|
|
13
|
+
*
|
|
14
|
+
* Envelope types and how we map them:
|
|
15
|
+
* - session_meta → carries payload.session_id + cwd; read the session id,
|
|
16
|
+
* emit nothing (the runner's SessionStart handler posts
|
|
17
|
+
* the attach entry).
|
|
18
|
+
* - response_item → the conversation itself (OpenAI Responses API items):
|
|
19
|
+
* payload.type 'message', role 'assistant' → assistant entry (has a
|
|
20
|
+
* stable `id` (msg_…) and a `phase`: 'commentary' | 'final_answer' -
|
|
21
|
+
* both are real assistant output, keep both).
|
|
22
|
+
* payload.type 'message', role 'user' | 'developer' → SKIP. Codex
|
|
23
|
+
* injects instructions and environment context as user/developer
|
|
24
|
+
* messages; the clean prompt arrives as an event_msg user_message.
|
|
25
|
+
* payload.type 'custom_tool_call' | 'function_call' → tool_use entry,
|
|
26
|
+
* keyed by the stable `call_id`.
|
|
27
|
+
* payload.type 'custom_tool_call_output' | 'function_call_output' →
|
|
28
|
+
* tool_result entry paired by `call_id`.
|
|
29
|
+
* payload.type 'reasoning' → SKIP (parity with the Claude parser).
|
|
30
|
+
* - event_msg → lifecycle/dup stream:
|
|
31
|
+
* payload.type 'user_message' → prompt entry (the clean user text).
|
|
32
|
+
* payload.type 'error' → system entry.
|
|
33
|
+
* everything else ('agent_message' duplicates the assistant
|
|
34
|
+
* response_item, 'token_count', 'task_started', 'task_complete') → SKIP.
|
|
35
|
+
* - turn_context / world_state → SKIP.
|
|
36
|
+
*
|
|
37
|
+
* Dedup/watermark: rollout lines carry no per-line uuid, so we synthesize.
|
|
38
|
+
* Entries prefer STABLE payload ids (assistant `msg_…` id, tool `call_id`) so
|
|
39
|
+
* a resumed session whose new rollout file replays history still dedupes
|
|
40
|
+
* across files; id-less entries (prompts) fall back to a positional
|
|
41
|
+
* `<session_id>#<lineIndex>` uuid. The WATERMARK is always positional
|
|
42
|
+
* (`<session_id>#<lineIndex>`): if the file rotated or the session id
|
|
43
|
+
* changed, the caller replays from the top and the server's source_uuid
|
|
44
|
+
* unique index collapses anything already forwarded.
|
|
45
|
+
*/
|
|
46
|
+
/** Join Responses-API content blocks ({type:'output_text'|'input_text'|'text', text}) */
|
|
47
|
+
function joinBlocks(content) {
|
|
48
|
+
if (typeof content === 'string')
|
|
49
|
+
return content;
|
|
50
|
+
if (!Array.isArray(content))
|
|
51
|
+
return '';
|
|
52
|
+
const parts = [];
|
|
53
|
+
for (const b of content) {
|
|
54
|
+
if (b && typeof b.text === 'string')
|
|
55
|
+
parts.push(b.text);
|
|
56
|
+
}
|
|
57
|
+
return parts.join('\n');
|
|
58
|
+
}
|
|
59
|
+
function positional(sessionId, idx) {
|
|
60
|
+
return `${sessionId}#${idx}`;
|
|
61
|
+
}
|
|
62
|
+
/** Parse `afterUuid` back into a line index IF it belongs to this session's
|
|
63
|
+
* positional scheme. Returns null for foreign/absent watermarks. */
|
|
64
|
+
function watermarkIndex(afterUuid, sessionId) {
|
|
65
|
+
if (!afterUuid || !sessionId)
|
|
66
|
+
return null;
|
|
67
|
+
const prefix = `${sessionId}#`;
|
|
68
|
+
if (!afterUuid.startsWith(prefix))
|
|
69
|
+
return null;
|
|
70
|
+
const idx = parseInt(afterUuid.slice(prefix.length), 10);
|
|
71
|
+
return Number.isInteger(idx) && idx >= 0 ? idx : null;
|
|
72
|
+
}
|
|
73
|
+
function lineToEntries(parsed, sessionId, idx, toolNames) {
|
|
74
|
+
const ts = typeof parsed?.timestamp === 'string' ? parsed.timestamp : undefined;
|
|
75
|
+
const payload = parsed?.payload;
|
|
76
|
+
if (!payload || typeof payload !== 'object')
|
|
77
|
+
return [];
|
|
78
|
+
if (parsed.type === 'response_item') {
|
|
79
|
+
const ptype = payload.type;
|
|
80
|
+
if (ptype === 'message' && payload.role === 'assistant') {
|
|
81
|
+
const text = joinBlocks(payload.content);
|
|
82
|
+
const blocks = [{ type: 'text', text }];
|
|
83
|
+
return [{
|
|
84
|
+
kind: 'assistant',
|
|
85
|
+
text,
|
|
86
|
+
blocks,
|
|
87
|
+
source_uuid: typeof payload.id === 'string' && payload.id ? payload.id : positional(sessionId, idx),
|
|
88
|
+
...(ts ? { ts } : {}),
|
|
89
|
+
}];
|
|
90
|
+
}
|
|
91
|
+
if (ptype === 'custom_tool_call' || ptype === 'function_call') {
|
|
92
|
+
const callId = typeof payload.call_id === 'string' ? payload.call_id : positional(sessionId, idx);
|
|
93
|
+
const toolName = typeof payload.name === 'string' && payload.name ? payload.name : 'tool';
|
|
94
|
+
toolNames.set(callId, toolName);
|
|
95
|
+
// custom_tool_call carries `input` (string); function_call carries
|
|
96
|
+
// `arguments` (JSON string). Surface parsed JSON when it parses.
|
|
97
|
+
let toolInput = payload.input ?? payload.arguments ?? null;
|
|
98
|
+
if (typeof toolInput === 'string') {
|
|
99
|
+
try {
|
|
100
|
+
toolInput = JSON.parse(toolInput);
|
|
101
|
+
}
|
|
102
|
+
catch { /* keep raw string */ }
|
|
103
|
+
}
|
|
104
|
+
return [{
|
|
105
|
+
kind: 'tool_use',
|
|
106
|
+
tool_use_id: callId,
|
|
107
|
+
tool_name: toolName,
|
|
108
|
+
tool_input: toolInput,
|
|
109
|
+
source_uuid: callId,
|
|
110
|
+
...(ts ? { ts } : {}),
|
|
111
|
+
}];
|
|
112
|
+
}
|
|
113
|
+
if (ptype === 'custom_tool_call_output' || ptype === 'function_call_output') {
|
|
114
|
+
const callId = typeof payload.call_id === 'string' ? payload.call_id : positional(sessionId, idx);
|
|
115
|
+
return [{
|
|
116
|
+
kind: 'tool_result',
|
|
117
|
+
tool_use_id: callId,
|
|
118
|
+
tool_name: toolNames.get(callId),
|
|
119
|
+
content: joinBlocks(payload.output) || (typeof payload.output === 'string' ? payload.output : null),
|
|
120
|
+
source_uuid: `${callId}#out`,
|
|
121
|
+
...(ts ? { ts } : {}),
|
|
122
|
+
}];
|
|
123
|
+
}
|
|
124
|
+
return []; // user/developer messages, reasoning, anything unknown
|
|
125
|
+
}
|
|
126
|
+
if (parsed.type === 'event_msg') {
|
|
127
|
+
if (payload.type === 'user_message' && typeof payload.message === 'string' && payload.message) {
|
|
128
|
+
return [{
|
|
129
|
+
kind: 'prompt',
|
|
130
|
+
prompt: payload.message,
|
|
131
|
+
source_uuid: positional(sessionId, idx),
|
|
132
|
+
...(ts ? { ts } : {}),
|
|
133
|
+
}];
|
|
134
|
+
}
|
|
135
|
+
if (payload.type === 'error') {
|
|
136
|
+
const msg = typeof payload.message === 'string' ? payload.message : JSON.stringify(payload);
|
|
137
|
+
return [{
|
|
138
|
+
kind: 'system',
|
|
139
|
+
content: `Codex error: ${msg}`,
|
|
140
|
+
source_uuid: positional(sessionId, idx),
|
|
141
|
+
...(ts ? { ts } : {}),
|
|
142
|
+
}];
|
|
143
|
+
}
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
return []; // session_meta, turn_context, world_state, unknown envelopes
|
|
147
|
+
}
|
|
148
|
+
/** Parse the full rollout JSONL and emit every ingest entry that comes
|
|
149
|
+
* *after* the positional watermark `afterUuid`. Same contract as the
|
|
150
|
+
* claude-code parser: returns entries in order, the new watermark, and
|
|
151
|
+
* whether the old watermark was still valid for this file. */
|
|
152
|
+
export function parseTranscript(content, afterUuid) {
|
|
153
|
+
const lines = content.split('\n');
|
|
154
|
+
// The session id lives in the first session_meta line. Without it we can
|
|
155
|
+
// still parse - positional uuids just key off an empty session id, and the
|
|
156
|
+
// stable payload ids carry dedup.
|
|
157
|
+
let sessionId = '';
|
|
158
|
+
for (const raw of lines) {
|
|
159
|
+
const line = raw.trim();
|
|
160
|
+
if (!line)
|
|
161
|
+
continue;
|
|
162
|
+
try {
|
|
163
|
+
const parsed = JSON.parse(line);
|
|
164
|
+
if (parsed?.type === 'session_meta' && typeof parsed.payload?.session_id === 'string') {
|
|
165
|
+
sessionId = parsed.payload.session_id;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch { /* not JSON - keep looking */ }
|
|
169
|
+
break; // only the first non-empty line can be session_meta
|
|
170
|
+
}
|
|
171
|
+
// A watermark pointing past EOF means the file was replaced by a SHORTER
|
|
172
|
+
// one under the same session id (a resume writes a fresh rollout; the old
|
|
173
|
+
// state file still carries the old file's position). Treat it as not-found
|
|
174
|
+
// so the caller replays from the top - otherwise every line of the new
|
|
175
|
+
// file sits "before" the watermark and capture wedges silently forever.
|
|
176
|
+
let startAfter = watermarkIndex(afterUuid, sessionId);
|
|
177
|
+
if (startAfter !== null && startAfter >= lines.length)
|
|
178
|
+
startAfter = null;
|
|
179
|
+
const foundWatermark = afterUuid === null || startAfter !== null;
|
|
180
|
+
const out = [];
|
|
181
|
+
const toolNames = new Map();
|
|
182
|
+
let lastIdx = startAfter;
|
|
183
|
+
for (let i = 0; i < lines.length; i++) {
|
|
184
|
+
const line = lines[i].trim();
|
|
185
|
+
if (!line)
|
|
186
|
+
continue;
|
|
187
|
+
let parsed;
|
|
188
|
+
try {
|
|
189
|
+
parsed = JSON.parse(line);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (startAfter !== null && i <= startAfter) {
|
|
195
|
+
// Pre-watermark: record tool names only, so a post-watermark
|
|
196
|
+
// tool_result still resolves the name of an already-forwarded call.
|
|
197
|
+
const p = parsed?.payload;
|
|
198
|
+
if (parsed?.type === 'response_item' && p &&
|
|
199
|
+
(p.type === 'custom_tool_call' || p.type === 'function_call') &&
|
|
200
|
+
typeof p.call_id === 'string') {
|
|
201
|
+
toolNames.set(p.call_id, typeof p.name === 'string' && p.name ? p.name : 'tool');
|
|
202
|
+
}
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const entries = lineToEntries(parsed, sessionId, i, toolNames);
|
|
206
|
+
for (const e of entries)
|
|
207
|
+
out.push(e);
|
|
208
|
+
lastIdx = i;
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
entries: out,
|
|
212
|
+
lastUuid: lastIdx === null ? afterUuid : positional(sessionId, lastIdx),
|
|
213
|
+
foundWatermark,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=codex.js.map
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grok Build chat_history.jsonl transcript → ingest entries.
|
|
3
|
+
*
|
|
4
|
+
* Grok Build persists each session under
|
|
5
|
+
* ~/.grok/sessions/<urlencoded-cwd>/<session_id>/
|
|
6
|
+
* with several files; `chat_history.jsonl` is the clean one-message-per-line
|
|
7
|
+
* conversation log and our parse target (events.jsonl is lifecycle noise,
|
|
8
|
+
* updates.jsonl is a chunked ACP stream). Verified against real sessions
|
|
9
|
+
* (Grok Build / grok-4.5, 2026-07-13). Line shapes:
|
|
10
|
+
*
|
|
11
|
+
* { type: 'system', content } → SKIP (system prompt)
|
|
12
|
+
* { type: 'user', content, synthetic_reason?, prompt_index? }
|
|
13
|
+
* synthetic_reason present → SKIP (injected context, not the user)
|
|
14
|
+
* else → prompt entry
|
|
15
|
+
* { type: 'assistant', content, tool_calls?: [{id,name,arguments}],
|
|
16
|
+
* model_id?, reasoning_effort? } → assistant (+ tool_use per call)
|
|
17
|
+
* { type: 'tool_result', tool_call_id, content } → tool_result
|
|
18
|
+
* { type: 'reasoning', encrypted_content, summary } → SKIP (parity with Claude parser)
|
|
19
|
+
*
|
|
20
|
+
* Grok hook payloads may not carry a transcript path; the capture runner
|
|
21
|
+
* derives it from the hook's cwd + session_id
|
|
22
|
+
* (`~/.grok/sessions/<encodeURIComponent(cwd)>/<session_id>/chat_history.jsonl`).
|
|
23
|
+
*
|
|
24
|
+
* Dedup/watermark: chat_history lines carry no uuid or timestamp, so uuids
|
|
25
|
+
* are synthesized. tool_use/tool_result key off the stable `tool_calls[].id`
|
|
26
|
+
* (`call-<uuid>-N`), which survives file rewrites; assistant/prompt entries
|
|
27
|
+
* use positional `<session_id>#<lineIndex>` uuids. The watermark is
|
|
28
|
+
* positional; a foreign watermark (session id mismatch / rotated file) makes
|
|
29
|
+
* the caller replay from the top, and the server's source_uuid unique index
|
|
30
|
+
* collapses anything already forwarded.
|
|
31
|
+
*/
|
|
32
|
+
function positional(sessionId, idx) {
|
|
33
|
+
return `${sessionId}#${idx}`;
|
|
34
|
+
}
|
|
35
|
+
function watermarkIndex(afterUuid, sessionId) {
|
|
36
|
+
if (!afterUuid || !sessionId)
|
|
37
|
+
return null;
|
|
38
|
+
const prefix = `${sessionId}#`;
|
|
39
|
+
if (!afterUuid.startsWith(prefix))
|
|
40
|
+
return null;
|
|
41
|
+
const idx = parseInt(afterUuid.slice(prefix.length), 10);
|
|
42
|
+
return Number.isInteger(idx) && idx >= 0 ? idx : null;
|
|
43
|
+
}
|
|
44
|
+
function contentToText(content) {
|
|
45
|
+
if (typeof content === 'string')
|
|
46
|
+
return content;
|
|
47
|
+
if (Array.isArray(content)) {
|
|
48
|
+
// Defensive: some harness versions may block-structure content.
|
|
49
|
+
const parts = [];
|
|
50
|
+
for (const b of content) {
|
|
51
|
+
if (b && typeof b.text === 'string')
|
|
52
|
+
parts.push(b.text);
|
|
53
|
+
}
|
|
54
|
+
return parts.join('\n');
|
|
55
|
+
}
|
|
56
|
+
return '';
|
|
57
|
+
}
|
|
58
|
+
function lineToEntries(parsed, sessionId, idx, toolNames) {
|
|
59
|
+
if (!parsed || typeof parsed !== 'object' || typeof parsed.type !== 'string')
|
|
60
|
+
return [];
|
|
61
|
+
if (parsed.type === 'user') {
|
|
62
|
+
if (parsed.synthetic_reason)
|
|
63
|
+
return []; // injected context, not the user
|
|
64
|
+
let prompt = contentToText(parsed.content);
|
|
65
|
+
if (!prompt)
|
|
66
|
+
return [];
|
|
67
|
+
// Grok also injects an environment block as a PLAIN user line (no
|
|
68
|
+
// synthetic_reason) at session start - recognizable by its wrapper tag.
|
|
69
|
+
if (prompt.startsWith('<user_info>'))
|
|
70
|
+
return [];
|
|
71
|
+
// The real prompt arrives wrapped in <user_query> tags - unwrap for
|
|
72
|
+
// clean display; the wrapper is Grok plumbing, not what the user typed.
|
|
73
|
+
const m = prompt.match(/^<user_query>\s*([\s\S]*?)\s*<\/user_query>$/);
|
|
74
|
+
if (m)
|
|
75
|
+
prompt = m[1];
|
|
76
|
+
if (!prompt)
|
|
77
|
+
return [];
|
|
78
|
+
return [{ kind: 'prompt', prompt, source_uuid: positional(sessionId, idx) }];
|
|
79
|
+
}
|
|
80
|
+
if (parsed.type === 'assistant') {
|
|
81
|
+
const text = contentToText(parsed.content);
|
|
82
|
+
const out = [];
|
|
83
|
+
const calls = Array.isArray(parsed.tool_calls) ? parsed.tool_calls : [];
|
|
84
|
+
if (text || calls.length) {
|
|
85
|
+
out.push({
|
|
86
|
+
kind: 'assistant',
|
|
87
|
+
text,
|
|
88
|
+
blocks: text ? [{ type: 'text', text }] : [],
|
|
89
|
+
...(typeof parsed.model_id === 'string' ? { model: parsed.model_id } : {}),
|
|
90
|
+
source_uuid: positional(sessionId, idx),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
for (const call of calls) {
|
|
94
|
+
if (!call || typeof call.id !== 'string' || !call.id)
|
|
95
|
+
continue;
|
|
96
|
+
const toolName = typeof call.name === 'string' && call.name ? call.name : 'tool';
|
|
97
|
+
toolNames.set(call.id, toolName);
|
|
98
|
+
let toolInput = call.arguments ?? null;
|
|
99
|
+
if (typeof toolInput === 'string') {
|
|
100
|
+
try {
|
|
101
|
+
toolInput = JSON.parse(toolInput);
|
|
102
|
+
}
|
|
103
|
+
catch { /* keep raw string */ }
|
|
104
|
+
}
|
|
105
|
+
out.push({
|
|
106
|
+
kind: 'tool_use',
|
|
107
|
+
tool_use_id: call.id,
|
|
108
|
+
tool_name: toolName,
|
|
109
|
+
tool_input: toolInput,
|
|
110
|
+
source_uuid: call.id,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
if (parsed.type === 'tool_result') {
|
|
116
|
+
if (typeof parsed.tool_call_id !== 'string' || !parsed.tool_call_id)
|
|
117
|
+
return [];
|
|
118
|
+
return [{
|
|
119
|
+
kind: 'tool_result',
|
|
120
|
+
tool_use_id: parsed.tool_call_id,
|
|
121
|
+
tool_name: toolNames.get(parsed.tool_call_id),
|
|
122
|
+
content: parsed.content ?? null,
|
|
123
|
+
source_uuid: `${parsed.tool_call_id}#out`,
|
|
124
|
+
}];
|
|
125
|
+
}
|
|
126
|
+
return []; // system, reasoning, unknown
|
|
127
|
+
}
|
|
128
|
+
/** Parse the full chat_history JSONL and emit every ingest entry that comes
|
|
129
|
+
* *after* the positional watermark `afterUuid`. Same contract as the
|
|
130
|
+
* claude-code parser. */
|
|
131
|
+
export function parseTranscript(content, afterUuid, ctx = {}) {
|
|
132
|
+
const sessionId = ctx.sessionId ?? '';
|
|
133
|
+
const lines = content.split('\n');
|
|
134
|
+
// A watermark pointing past EOF means the file shrank under the same
|
|
135
|
+
// session id (e.g. a rewind rewrote chat_history). Treat it as not-found
|
|
136
|
+
// so the caller replays from the top - otherwise capture wedges silently.
|
|
137
|
+
let startAfter = watermarkIndex(afterUuid, sessionId);
|
|
138
|
+
if (startAfter !== null && startAfter >= lines.length)
|
|
139
|
+
startAfter = null;
|
|
140
|
+
const foundWatermark = afterUuid === null || startAfter !== null;
|
|
141
|
+
const out = [];
|
|
142
|
+
const toolNames = new Map();
|
|
143
|
+
let lastIdx = startAfter;
|
|
144
|
+
for (let i = 0; i < lines.length; i++) {
|
|
145
|
+
const line = lines[i].trim();
|
|
146
|
+
if (!line)
|
|
147
|
+
continue;
|
|
148
|
+
let parsed;
|
|
149
|
+
try {
|
|
150
|
+
parsed = JSON.parse(line);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (startAfter !== null && i <= startAfter) {
|
|
156
|
+
// Pre-watermark: record tool names only, so a post-watermark
|
|
157
|
+
// tool_result still resolves the name of an already-forwarded call.
|
|
158
|
+
if (parsed?.type === 'assistant' && Array.isArray(parsed.tool_calls)) {
|
|
159
|
+
for (const call of parsed.tool_calls) {
|
|
160
|
+
if (call && typeof call.id === 'string' && call.id) {
|
|
161
|
+
toolNames.set(call.id, typeof call.name === 'string' && call.name ? call.name : 'tool');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const entries = lineToEntries(parsed, sessionId, i, toolNames);
|
|
168
|
+
for (const e of entries)
|
|
169
|
+
out.push(e);
|
|
170
|
+
lastIdx = i;
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
entries: out,
|
|
174
|
+
lastUuid: lastIdx === null ? afterUuid : positional(sessionId, lastIdx),
|
|
175
|
+
foundWatermark,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=grok.js.map
|
package/dist/client-context.js
CHANGED
|
@@ -27,6 +27,8 @@ export function detectHarness() {
|
|
|
27
27
|
}
|
|
28
28
|
if (hasEnvPrefix('CODEX_'))
|
|
29
29
|
return { harness: 'codex' };
|
|
30
|
+
if (hasEnvPrefix('GROK_'))
|
|
31
|
+
return { harness: 'grok', harnessSession: process.env.GROK_SESSION_ID };
|
|
30
32
|
if (process.env.CURSOR_TRACE_ID || hasEnvPrefix('CURSOR_') || (process.env.TERM_PROGRAM ?? '').toLowerCase().includes('cursor')) {
|
|
31
33
|
return { harness: 'cursor' };
|
|
32
34
|
}
|