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
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
|
|
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
|
|
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
|
-
|
|
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
|
|
34
|
+
gipity build
|
|
25
35
|
```
|
|
26
36
|
|
|
27
|
-
`gipity
|
|
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
|
-
##
|
|
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 [
|
|
96
|
+
Project name [project-001]: cool-app
|
|
76
97
|
Creating "cool-app"...
|
|
77
98
|
Created.
|
|
78
99
|
|
|
79
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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()
|
|
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
|