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.
Files changed (40) hide show
  1. package/dist/agents/claude-code.js +53 -0
  2. package/dist/agents/codex.js +49 -0
  3. package/dist/agents/grok.js +54 -0
  4. package/dist/agents/index.js +23 -0
  5. package/dist/agents/types.js +18 -0
  6. package/dist/api.js +7 -0
  7. package/dist/capture/sources/codex.js +216 -0
  8. package/dist/capture/sources/grok.js +178 -0
  9. package/dist/client-context.js +2 -0
  10. package/dist/commands/build.js +1352 -0
  11. package/dist/commands/chat.js +9 -3
  12. package/dist/commands/claude.js +4 -13
  13. package/dist/commands/db.js +12 -5
  14. package/dist/commands/deploy.js +19 -1
  15. package/dist/commands/doctor.js +8 -2
  16. package/dist/commands/generate.js +61 -4
  17. package/dist/commands/init.js +9 -15
  18. package/dist/commands/page-eval.js +404 -56
  19. package/dist/commands/page-inspect.js +15 -5
  20. package/dist/commands/page-screenshot.js +269 -64
  21. package/dist/commands/project.js +1 -1
  22. package/dist/commands/relay-install.js +1 -1
  23. package/dist/commands/relay.js +2 -2
  24. package/dist/commands/sandbox.js +62 -16
  25. package/dist/commands/setup.js +16 -10
  26. package/dist/commands/uninstall.js +25 -3
  27. package/dist/hooks/capture-runner.js +136 -39
  28. package/dist/index.js +1962 -668
  29. package/dist/knowledge.js +3 -3
  30. package/dist/page-fixtures.js +92 -3
  31. package/dist/prefs.js +50 -0
  32. package/dist/project-setup.js +2 -10
  33. package/dist/provider-docs.js +10 -10
  34. package/dist/relay/daemon.js +140 -18
  35. package/dist/relay/device-http.js +22 -0
  36. package/dist/relay/diagnostics.js +4 -2
  37. package/dist/relay/media-upload.js +184 -0
  38. package/dist/relay/onboarding.js +2 -2
  39. package/dist/setup.js +262 -18
  40. 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
@@ -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
  }