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
package/README.md CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  The full-stack platform tuned for AI agents.
4
4
 
5
- [Gipity](https://gipity.ai) is the platform: hosting, databases, file storage, deployment, workflows, code execution, and monitoring. Agent-tuned from idea to deploy. Use standalone, or pair with Claude Code to give your local agent cloud superpowers. Any model, any infra, always your code.
5
+ [Gipity](https://gipity.ai) is the platform: hosting, databases, file storage, deployment, workflows, code execution, and monitoring. Agent-tuned from idea to deploy. Use standalone, or pair with your coding agent - Claude Code, Codex, or Grok - to give it cloud superpowers. Any model, any infra, always your code.
6
6
 
7
- This CLI connects [Claude Code](https://claude.ai/claude-code) to Gipity's cloud platform - databases, deployment, browser testing, image gen, and 50+ other capabilities your local agent doesn't have. It also syncs files so Claude Code and the Gipity web agent share the same project.
7
+ This CLI connects your coding agent - [Claude Code](https://claude.ai/claude-code), Codex, or Grok - to Gipity's cloud platform: databases, deployment, browser testing, image gen, and 50+ other capabilities your local agent doesn't have. It also syncs files so your local agent and the Gipity web agent share the same project.
8
8
 
9
9
  ## Getting Started
10
10
 
11
- One line installs everything. It sets up Node 18+ (if you don't already have it) and the Gipity CLI, with no sudo required:
11
+ **Step 1 - install.** One line installs everything. It sets up Node 18+ (if you don't already have it) and the Gipity CLI, with no sudo required:
12
12
 
13
13
  ```bash
14
14
  # macOS / Linux / WSL
@@ -18,13 +18,42 @@ curl -fsSL https://gipity.ai/install.sh | bash
18
18
  irm https://gipity.ai/install.ps1 | iex
19
19
  ```
20
20
 
21
- Then launch your coding agent wired into Gipity:
21
+ **Step 2 - pick your path.** There are three, and they mix freely:
22
+
23
+ | You want to... | Run |
24
+ |----------------|-----|
25
+ | Start building right now, from anywhere | `gipity build` |
26
+ | Use your own workflow in your own directory | `gipity init`, then `claude` / `codex` / `grok` |
27
+ | Drive this computer from gipity.ai (phone, browser) | `gipity connect` |
28
+
29
+ ### `gipity build` - start from anywhere
30
+
31
+ One command does everything: logs you in (6-digit email code), lets you pick or create a project, lets you pick your coding agent (Claude Code, Codex, or Grok - it installs the agent if needed), and launches it with the whole Gipity stack wired up.
22
32
 
23
33
  ```bash
24
- gipity claude
34
+ gipity build
25
35
  ```
26
36
 
27
- `gipity claude` walks you through login, project setup, and launches Claude Code. Using Codex, Gemini, or Cursor instead? Run `gipity init`.
37
+ ### `gipity init` - bring your own workflow
38
+
39
+ Prefer launching your agent yourself? From any project directory:
40
+
41
+ ```bash
42
+ gipity init
43
+ claude # or codex, or grok - whatever you use
44
+ ```
45
+
46
+ `init` links the directory to a Gipity project, writes CLAUDE.md/AGENTS.md primers so your agent understands Gipity, and installs the Gipity skills + file-sync hooks into the agent CLIs found on your machine (Claude Code, Codex, Grok; Cursor and Gemini get primer files too).
47
+
48
+ ### `gipity connect` - drive it from the web
49
+
50
+ Want to start chats from gipity.ai (including your phone) and have them run the coding agent on this computer? Connect it once:
51
+
52
+ ```bash
53
+ gipity connect
54
+ ```
55
+
56
+ It logs you in, pairs the machine, starts the Gipity relay in the background, and installs the login service so it survives reboots - then stops, without launching anything. From then on, open gipity.ai and start a chat to drive your agent here. Manage it with `gipity relay status` / `pause` / `resume` / `revoke`.
28
57
 
29
58
  ### Prefer npm
30
59
 
@@ -47,15 +76,7 @@ gipity update # force an immediate update now
47
76
 
48
77
  To opt out: `export DISABLE_AUTOUPDATER=1` (matches Claude Code), or set `{ "autoUpdates": false }` in `~/.gipity/settings.json`. CI environments are auto-detected and skipped.
49
78
 
50
- ## Quick Start
51
-
52
- One command. It walks you through login, project setup, and drops you into Claude Code.
53
-
54
- ```bash
55
- gipity claude --dangerously-skip-permissions
56
- ```
57
-
58
- That's it. You'll see:
79
+ ## What `gipity build` looks like
59
80
 
60
81
  ```
61
82
  Welcome to Gipity
@@ -72,16 +93,29 @@ That's it. You'll see:
72
93
  2. Create new project
73
94
 
74
95
  Choose (1-2): 2
75
- Project name [project01]: cool-app
96
+ Project name [project-001]: cool-app
76
97
  Creating "cool-app"...
77
98
  Created.
78
99
 
79
- Launching Claude Code...
100
+ Which coding agent?
101
+ 1. Claude Code (Anthropic)
102
+ 2. Codex (OpenAI)
103
+ 3. Grok (xAI)
104
+
105
+ Launching Claude Code, powered by Gipity.
80
106
  ```
81
107
 
82
- If you're already logged in, it skips straight to project setup. If you already have a project in the current directory, it skips straight to launching Claude Code.
108
+ If you're already logged in, it skips straight to the project picker. If you're already inside a Gipity project directory, it uses that project. Your last-used agent is the default next time.
109
+
110
+ Projects live in `~/GipityProjects/{project-slug}/` - created automatically on first use. Any extra flags (like `--model opus`) pass straight through to the agent. Useful flags:
83
111
 
84
- Projects live in `~/GipityProjects/{project-slug}/` - created automatically on first use. Any extra flags (like `--dangerously-skip-permissions`, `--model opus`, etc.) get passed through to Claude.
112
+ ```bash
113
+ gipity build --agent codex # Skip the agent picker
114
+ gipity build --new-project --name app # Create a fresh project without the picker
115
+ gipity build --project my-app # Open a specific project
116
+ gipity build --here # Use the current directory, not ~/GipityProjects/
117
+ gipity build -p "add a contact form" # Headless one-shot (no interactive session)
118
+ ```
85
119
 
86
120
  ### The manual way
87
121
 
@@ -92,26 +126,28 @@ gipity login --email you@example.com
92
126
  gipity login --code 123456
93
127
  cd my-project
94
128
  gipity init
95
- claude
129
+ claude # or codex, or grok
96
130
  ```
97
131
 
98
- ## Claude Code Integration
132
+ ## Coding Agent Integration
133
+
134
+ This is the good part. When you run `gipity init` (or `gipity build`) in a project, it wires two hooks into your agent (Claude Code, Codex, and Grok all get them):
99
135
 
100
- This is the good part. When you run `gipity init` in a project, it sets up two hooks in `.claude/settings.json`:
136
+ **Auto-push** - Every time your agent writes or edits a file, it gets pushed to Gipity in the background. No extra steps.
101
137
 
102
- **Auto-push** - Every time Claude Code writes or edits a file, it gets pushed to Gipity in the background. No extra steps.
138
+ **Auto-pull** - Before each turn, your agent pulls any changes that happened remotely (like if your Gipity agent built something via chat). It sees what changed and can pick up where things left off.
103
139
 
104
- **Auto-pull** - Before each turn, Claude Code pulls any changes that happened remotely (like if your Gipity agent built something via chat). Claude sees what changed and can pick up where things left off.
140
+ That means your local agent and your Gipity agent share the same files, same project, same context. You get the best of both - hands-on coding locally, autonomous agent work on Gipity.
105
141
 
106
- That means Claude Code and your Gipity agent share the same files, same project, same context. You get the best of both - Claude Code for hands-on coding, Gipity for autonomous agent work.
142
+ Sessions are also recorded to your Gipity project so you can watch them live at prompt.gipity.ai. Opt out per project with `gipity init --no-capture`.
107
143
 
108
144
  ### What gets set up
109
145
 
110
146
  ```
111
147
  .gipity.json # Project config (which project, which agent)
112
148
  .gipity/ # Local sync state (gitignored)
113
- .claude/settings.json # Hooks for auto-push and auto-pull
114
- CLAUDE.md # Gipity commands reference for Claude Code
149
+ .claude/settings.json # Hooks for auto-push and auto-pull (per-agent equivalents for Codex/Grok)
150
+ CLAUDE.md / AGENTS.md # Gipity commands reference for your agent
115
151
  ```
116
152
 
117
153
  ### Manual sync
@@ -128,9 +164,11 @@ gipity sync down # Pull remote changes
128
164
 
129
165
  | Command | What it does |
130
166
  |---------|-------------|
131
- | `gipity claude` | Log in, pick a project, and launch Claude Code - all in one |
167
+ | `gipity build` | Log in, pick a project, pick your coding agent, and launch it - all in one |
168
+ | `gipity init` | Link this directory to a project and set up your coding agent |
169
+ | `gipity connect` | Connect this computer to gipity.ai so the web CLI can drive it |
170
+ | `gipity relay` | Manage the relay (status, pause, resume, revoke) |
132
171
  | `gipity login` | Authenticate with email + verification code |
133
- | `gipity init` | Set up a Gipity project and configure Claude Code |
134
172
  | `gipity status` | Show project, agent, and auth info |
135
173
  | `gipity sync` | Sync files between local and Gipity |
136
174
  | `gipity push <file>` | Push a single file |
@@ -0,0 +1,48 @@
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
+ buildInteractiveArgs({ resume, model }) {
9
+ const args = [];
10
+ if (model)
11
+ args.push('--model', model);
12
+ if (resume)
13
+ args.push('--resume', resume);
14
+ return args;
15
+ },
16
+ buildHeadlessArgs({ message, resume, model, bypassApprovals, jsonStream }) {
17
+ const args = ['-p', message];
18
+ if (bypassApprovals)
19
+ args.push('--permission-mode', 'bypassPermissions');
20
+ if (model)
21
+ args.push('--model', model);
22
+ if (resume)
23
+ args.push('--resume', resume);
24
+ // `--verbose` is required by Claude Code when combining -p with
25
+ // --output-format stream-json; partial messages feed live typing.
26
+ if (jsonStream)
27
+ args.push('--output-format', 'stream-json', '--verbose', '--include-partial-messages');
28
+ return args;
29
+ },
30
+ sessionIdFromStreamEvent(event) {
31
+ // Claude's stream-json: the system/init event (and the result footer)
32
+ // carry `session_id`.
33
+ if (event && typeof event.session_id === 'string' && event.session_id)
34
+ return event.session_id;
35
+ return null;
36
+ },
37
+ hooksSupportedOnPlatform() {
38
+ return true;
39
+ },
40
+ // The daemon parses Claude's stream-json into ingest entries itself
41
+ // (hook capture stands down via GIPITY_CAPTURE=off on dispatches).
42
+ daemonStreamCapture: true,
43
+ ensureInstalled() {
44
+ return ensureClaudeInstalled().installed;
45
+ },
46
+ installHint: `npm install -g ${CLAUDE_PACKAGE}`,
47
+ };
48
+ //# sourceMappingURL=claude-code.js.map
@@ -0,0 +1,45 @@
1
+ export const codexAdapter = {
2
+ key: 'codex',
3
+ source: 'codex',
4
+ displayName: 'Codex',
5
+ providerName: 'OpenAI',
6
+ binary: 'codex',
7
+ buildInteractiveArgs({ resume, model }) {
8
+ const args = [];
9
+ if (model)
10
+ args.push('--model', model);
11
+ if (resume)
12
+ args.push('resume', resume);
13
+ return args;
14
+ },
15
+ buildHeadlessArgs({ message, resume, model, bypassApprovals, jsonStream }) {
16
+ // `codex exec resume <sid> "<msg>"` continues a session; plain
17
+ // `codex exec "<msg>"` starts fresh. Flags are shared by both forms.
18
+ const args = resume ? ['exec', 'resume', resume, message] : ['exec', message];
19
+ if (model)
20
+ args.push('--model', model);
21
+ if (bypassApprovals) {
22
+ args.push('-s', 'workspace-write', '-c', 'sandbox_workspace_write.network_access=true', '--dangerously-bypass-hook-trust', '--skip-git-repo-check');
23
+ }
24
+ if (jsonStream)
25
+ args.push('--json');
26
+ return args;
27
+ },
28
+ sessionIdFromStreamEvent(event) {
29
+ // `codex exec --json`: the thread.started event carries thread_id.
30
+ if (event?.type === 'thread.started' && typeof event.thread_id === 'string') {
31
+ return event.thread_id;
32
+ }
33
+ return null;
34
+ },
35
+ hooksSupportedOnPlatform(platform) {
36
+ return platform !== 'win32'; // Codex hooks are disabled on Windows
37
+ },
38
+ // No stream-json ingest mapper for Codex: relay dispatches keep hook
39
+ // capture ON (GIPITY_CONVERSATION_GUID binds it) and the transcript
40
+ // parser mirrors the session; the daemon tracks byte-level progress only.
41
+ daemonStreamCapture: false,
42
+ 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.',
43
+ installHint: 'npm install -g @openai/codex',
44
+ };
45
+ //# sourceMappingURL=codex.js.map
@@ -0,0 +1,50 @@
1
+ export const grokAdapter = {
2
+ key: 'grok',
3
+ source: 'grok',
4
+ displayName: 'Grok',
5
+ providerName: 'xAI',
6
+ binary: 'grok',
7
+ buildInteractiveArgs({ resume, model }) {
8
+ const args = [];
9
+ if (model)
10
+ args.push('--model', model);
11
+ if (resume)
12
+ args.push('--resume', resume);
13
+ return args;
14
+ },
15
+ buildHeadlessArgs({ message, resume, model, bypassApprovals, jsonStream }) {
16
+ const args = ['-p', message];
17
+ if (model)
18
+ args.push('--model', model);
19
+ if (resume)
20
+ args.push('--resume', resume);
21
+ if (bypassApprovals)
22
+ args.push('--always-approve');
23
+ if (jsonStream)
24
+ args.push('--output-format', 'streaming-json');
25
+ return args;
26
+ },
27
+ sessionIdFromStreamEvent(event) {
28
+ // Grok's streaming-json is ACP-shaped: session/update notifications
29
+ // carry params.sessionId. Accept a top-level sessionId too, defensively.
30
+ const fromParams = event?.params?.sessionId;
31
+ if (typeof fromParams === 'string' && fromParams)
32
+ return fromParams;
33
+ if (typeof event?.sessionId === 'string' && event.sessionId)
34
+ return event.sessionId;
35
+ return null;
36
+ },
37
+ hooksSupportedOnPlatform() {
38
+ return true; // in the interactive TUI; headless runs use headlessCapture
39
+ },
40
+ daemonStreamCapture: false,
41
+ // Verified live (2026-07-13): grok -p fires NO plugin hooks - not even
42
+ // SessionStart/Stop - so headless capture is launcher-driven: pin the
43
+ // session id, then replay ~/.grok/sessions/<cwd>/<sid>/chat_history.jsonl
44
+ // through the capture runner after the run.
45
+ headlessCapture: {
46
+ sessionIdArgs: (sessionId) => ['--session-id', sessionId],
47
+ },
48
+ installHint: 'curl -fsSL https://grok.x.ai/install.sh | sh (or see docs.x.ai/grok-build)',
49
+ };
50
+ //# 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
@@ -56,8 +56,8 @@ async function bearerToken() {
56
56
  /** True when a long-lived agent API token (GIPITY_TOKEN) is in play. Such a token
57
57
  * is static — it can't be refreshed — so the 401 self-heal path must skip it and
58
58
  * the "run: gipity login" hint must not be shown (that's the wrong recovery for
59
- * env-token callers). */
60
- function usingEnvToken() {
59
+ * env-token callers). Exported so `gipity status` can name the auth source. */
60
+ export function usingEnvToken() {
61
61
  return !!process.env.GIPITY_TOKEN?.trim();
62
62
  }
63
63
  // A transient 401 on a session token means the access token the server just saw
@@ -83,9 +83,12 @@ async function shouldRetryAfter401(status, retried) {
83
83
  /** Append a re-login hint to a 401 message for session users. A 401 that
84
84
  * survived the refresh-and-retry is an unrecoverable session (the refresh token
85
85
  * is gone too); env-token callers authenticate differently, so don't misdirect
86
- * them. */
86
+ * them. Names the headless path too — an unattended agent has no mailbox for
87
+ * the emailed login code, and dead-ending there stranded whole runs (cli#137). */
87
88
  function with401Hint(status, message) {
88
- return status === 401 && !usingEnvToken() ? `${message} — run: gipity login` : message;
89
+ return status === 401 && !usingEnvToken()
90
+ ? `${message} — run: gipity login (headless/CI: set GIPITY_TOKEN instead — gipity skill read agent-deploy)`
91
+ : message;
89
92
  }
90
93
  async function getHeaders() {
91
94
  return {
@@ -195,6 +198,13 @@ export async function sendMessage(message) {
195
198
  if (res.data.conversationGuid !== config.conversationGuid) {
196
199
  saveConfig({ ...config, conversationGuid: res.data.conversationGuid });
197
200
  }
201
+ // The server's cost-consent gate stops the agentic loop on big-build
202
+ // requests and returns an explicit costWarning with empty content. Surface
203
+ // it - before this the CLI printed nothing and the command silently did
204
+ // nothing. Replying "y" (next sendMessage) consents and proceeds.
205
+ if (res.data.costWarning && !res.data.content) {
206
+ return res.data.costWarning.message;
207
+ }
198
208
  return res.data.content;
199
209
  }
200
210
  /** 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