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
package/dist/knowledge.js
CHANGED
|
@@ -54,7 +54,7 @@ Prefer the cheapest option that works - CLI and sandbox are instant and free, ap
|
|
|
54
54
|
3. App services - runtime HTTP endpoints your deployed app calls directly at \`https://a.gipity.ai/api/<PROJECT_GUID>/services/*\`. Available: LLM, TTS, image, sound, music, transcribe, video, file upload, realtime, location, push notifications (Gipity Notify - \`gipity add notify\`, send from a function with the injected \`notify()\`; see \`app-notify\`). Load the matching skill (\`app-llm\`, \`app-tts\`, etc.) before writing service code - they have the schemas, auth pattern, and common-mistake guards. For one-off generation during development, prefer \`gipity generate <image|video|speech|sound|music>\` or \`gipity chat\` - direct generation always bills you (the owner), regardless of any service \`billing_mode\` (that setting only governs the deployed app's runtime calls; never flip it just to create assets). \`gipity generate\` saves to a generic file in the current directory by default (e.g. \`./generated.png\`) - pass \`-o <path>\` to write it straight into your source tree so it deploys (e.g. \`gipity generate image "hero banner" -o src/assets/images/hero.png\`) instead of generating at cwd and moving it.
|
|
55
55
|
4. Delegate to Gip (\`gipity chat "<task>"\`) - only when the work genuinely needs agent reasoning or a tool not in the CLI, sandbox, or app services. Required for: Twitter/X search, Gmail, calendar, push notifications, video understanding, audio source isolation, cross-model second opinions, multi-step orchestration. Don't use \`gipity chat\` for anything the sandbox can do - it's slower and burns tokens.
|
|
56
56
|
|
|
57
|
-
You are the developer. Write files in this directory -
|
|
57
|
+
You are the developer. Write files in this directory - Gipity's editor hooks (installed by \`gipity init\` into Claude Code, Grok, and Codex) auto-sync them to Gipity. Don't run \`npm install\`, \`npm start\`, \`node\`, or \`python\` locally; there is no local runtime. Code runs in the Gipity sandbox.
|
|
58
58
|
|
|
59
59
|
## Use first-party services before reaching outside
|
|
60
60
|
|
|
@@ -90,7 +90,7 @@ The full "when to add a template" rule and the definition of done are spelled ou
|
|
|
90
90
|
|
|
91
91
|
Build loop: \`gipity add\` → edit files → \`gipity deploy dev --inspect\` → fix any errors → repeat until the definition of done is met. \`deploy --inspect\` deploys and then runs the page-inspect report on the live URL in one command; use a standalone \`gipity page inspect <url>\` only when re-checking without deploying.
|
|
92
92
|
|
|
93
|
-
\`add\` writes real files to disk - Read a scaffolded file before your first Write/Edit to it, or the call fails \`"File has not been read yet"
|
|
93
|
+
\`add\` writes real files to disk - Read a scaffolded file with the Read tool before your first Write/Edit to it, or the call fails \`"File has not been read yet"\` (viewing it with \`cat\`/\`head\` in Bash does not count - the harness only tracks the Read tool). Don't rewrite from memory of the template.
|
|
94
94
|
|
|
95
95
|
Make your file changes and verify they landed, then run \`gipity deploy dev\` once. \`0 uploaded, N unchanged\` means nothing changed on disk - fix the files, don't re-run deploy or probe the environment.
|
|
96
96
|
|
|
@@ -146,7 +146,7 @@ Every tool call returns its full output with that call. There is no output buffe
|
|
|
146
146
|
|
|
147
147
|
This directory is the app root - it holds \`.gipity.json\` (and \`gipity.yaml\` for backends) and is already your cwd, so run app commands here. Don't \`cd\` to a git root (\`git rev-parse --show-toplevel\`): when the app is nested in a larger repo that resolves outside the app.
|
|
148
148
|
|
|
149
|
-
Write files locally -
|
|
149
|
+
Write files locally - Gipity's editor hooks (installed into Claude Code, Grok, and Codex by \`gipity init\`) auto-push every save to Gipity and auto-pull remote changes (images, audio from \`gipity chat\`) before each turn. Use \`gipity sync\` if things get out of sync (or if the hooks aren't installed - \`gipity status --repair-hooks\` re-enables them). Deletes are safe - use \`rollback\` with a datetime to undo, or \`file_version_restore\` for individual files.
|
|
150
150
|
|
|
151
151
|
To keep local-only material (research clones, scratch data, vendored references) in the project directory without syncing or deploying it, list it in a \`.gipityignore\` at the project root - gitignore-style, one pattern per line, \`#\` comments. Ignored paths are invisible to sync in both directions; anything that already synced before being ignored stays on the server until you delete it.
|
|
152
152
|
|
package/dist/page-fixtures.js
CHANGED
|
@@ -8,12 +8,63 @@
|
|
|
8
8
|
// Uses the same presigned init -> PUT -> complete flow the app file-upload
|
|
9
9
|
// service exposes, with `public: true`. Cleanup goes through the matching
|
|
10
10
|
// DELETE /api/:appGuid/uploads/:guid, which removes the public object too.
|
|
11
|
-
import { readFileSync, statSync } from 'node:fs';
|
|
12
|
-
import { basename } from 'node:path';
|
|
11
|
+
import { readFileSync, statSync, existsSync, readdirSync } from 'node:fs';
|
|
12
|
+
import { basename, resolve, relative, join } from 'node:path';
|
|
13
13
|
import { post, del } from './api.js';
|
|
14
|
+
import { getProjectRoot } from './config.js';
|
|
14
15
|
import { guessMime } from './upload.js';
|
|
16
|
+
/** Dirs a hand-made asset is never in, and that are expensive to walk. */
|
|
17
|
+
const FIND_SKIP = new Set(['node_modules', '.git', '.gipity', 'dist', 'build', '.next', 'coverage']);
|
|
18
|
+
/** Find files named `name` anywhere under `root` (breadth-first, bounded). */
|
|
19
|
+
function findByBasename(root, name, limit = 3) {
|
|
20
|
+
const hits = [];
|
|
21
|
+
const queue = [root];
|
|
22
|
+
let visited = 0;
|
|
23
|
+
while (queue.length && hits.length < limit && visited < 500) {
|
|
24
|
+
const dir = queue.shift();
|
|
25
|
+
visited++;
|
|
26
|
+
let entries;
|
|
27
|
+
try {
|
|
28
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
for (const e of entries) {
|
|
34
|
+
if (e.isDirectory()) {
|
|
35
|
+
if (!FIND_SKIP.has(e.name))
|
|
36
|
+
queue.push(join(dir, e.name));
|
|
37
|
+
}
|
|
38
|
+
else if (e.name === name && hits.length < limit) {
|
|
39
|
+
hits.push(join(dir, e.name));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return hits;
|
|
44
|
+
}
|
|
45
|
+
/** Every `page` flag that takes a local file resolves it against the CURRENT
|
|
46
|
+
* DIRECTORY, and a bare ENOENT from deep inside the upload path ("stat
|
|
47
|
+
* './tmp/fist.jpg'") tells the caller nothing about where we looked or where
|
|
48
|
+
* the file actually is. That costs an ls/find round-trip every time a file was
|
|
49
|
+
* written somewhere other than where the caller assumed - which is exactly what
|
|
50
|
+
* happens after a `cd` that failed, or a `gipity generate` run from a different
|
|
51
|
+
* directory. So: name the absolute path we tried, the directory it was resolved
|
|
52
|
+
* against, and - when a file with that basename exists elsewhere in the project
|
|
53
|
+
* - the path that would have worked. Recovery becomes copy-paste, not a search. */
|
|
54
|
+
export function assertLocalAsset(flag, localPath) {
|
|
55
|
+
const abs = resolve(localPath);
|
|
56
|
+
if (existsSync(abs))
|
|
57
|
+
return;
|
|
58
|
+
const root = getProjectRoot() ?? process.cwd();
|
|
59
|
+
const elsewhere = findByBasename(root, basename(localPath)).filter(p => p !== abs);
|
|
60
|
+
const found = elsewhere.length
|
|
61
|
+
? `\nThat file DOES exist here — pass this path instead:\n${elsewhere.map(p => ` ${flag} ${relative(process.cwd(), p) || p}`).join('\n')}`
|
|
62
|
+
: `\nNothing named "${basename(localPath)}" under ${root} either. Generate a frame with \`gipity generate image "<description>" -o ${localPath}\`, or check the path.`;
|
|
63
|
+
throw new Error(`${flag} ${localPath}: no such file — looked for ${abs} (relative paths resolve against the current directory, ${process.cwd()}).${found}`);
|
|
64
|
+
}
|
|
15
65
|
/** Upload a local file to the app's public file store and return its URL. */
|
|
16
|
-
export async function uploadPublicFixture(projectGuid, localPath) {
|
|
66
|
+
export async function uploadPublicFixture(projectGuid, localPath, flag = '--fixture') {
|
|
67
|
+
assertLocalAsset(flag, localPath);
|
|
17
68
|
const name = basename(localPath);
|
|
18
69
|
const size = statSync(localPath).size;
|
|
19
70
|
const contentType = guessMime(localPath);
|
|
@@ -38,4 +89,42 @@ export async function uploadPublicFixture(projectGuid, localPath) {
|
|
|
38
89
|
export async function deleteFixture(projectGuid, guid) {
|
|
39
90
|
await del(`/api/${projectGuid}/uploads/${guid}`);
|
|
40
91
|
}
|
|
92
|
+
// ── camera feed ─────────────────────────────────────────────────────────────
|
|
93
|
+
// A headless browser has no webcam, so a vision app (MediaPipe, YOLOX, any
|
|
94
|
+
// getUserMedia consumer) can't be exercised at all: getUserMedia rejects, the
|
|
95
|
+
// app shows its no-camera state, and the code path the user actually cares
|
|
96
|
+
// about never runs. `--fake-media` alone only fixes the rejection — Chrome's
|
|
97
|
+
// built-in synthetic device is a rolling test pattern, so a gesture/pose/object
|
|
98
|
+
// model still sees nothing recognizable and every agent ends up stubbing the
|
|
99
|
+
// model's own output to test around it.
|
|
100
|
+
//
|
|
101
|
+
// --camera closes that: host a real image/video and let the browser play it as
|
|
102
|
+
// the webcam's frames, so the app's REAL pipeline (frames → model → app logic)
|
|
103
|
+
// runs headlessly on input you control. Hosting reuses the fixture path above;
|
|
104
|
+
// the browser side (fetch the file, feed it to Chrome's fake capture device)
|
|
105
|
+
// is the server's job — the CLI just names the file and hands over its URL.
|
|
106
|
+
/** Container formats Chrome's fake video-capture device can be fed from, plus
|
|
107
|
+
* the still-image types the server transcodes into a looping single-frame
|
|
108
|
+
* feed. Anything else is rejected here (fast, local) rather than after an
|
|
109
|
+
* upload + a browser launch. */
|
|
110
|
+
const CAMERA_EXTS = ['.png', '.jpg', '.jpeg', '.webp', '.mp4', '.webm', '.y4m', '.mjpeg'];
|
|
111
|
+
/** Validate a --camera path before uploading it. Throws with the accepted list
|
|
112
|
+
* and the in-platform way to produce a frame, so a wrong file type costs one
|
|
113
|
+
* local error instead of an upload plus an opaque browser failure. */
|
|
114
|
+
export function assertCameraFile(localPath) {
|
|
115
|
+
assertLocalAsset('--camera', localPath);
|
|
116
|
+
const ext = localPath.slice(localPath.lastIndexOf('.')).toLowerCase();
|
|
117
|
+
if (CAMERA_EXTS.includes(ext))
|
|
118
|
+
return;
|
|
119
|
+
throw new Error(`--camera ${basename(localPath)}: unsupported file type "${ext || '(none)'}" — the camera feed must be an image or video (${CAMERA_EXTS.join(', ')}).\n` +
|
|
120
|
+
`A still image is played as a looping single-frame feed, which is what a gesture/pose/object model needs.\n` +
|
|
121
|
+
`No frame to hand? Generate one: gipity generate image "a hand making a closed fist, palm to camera, plain background"`);
|
|
122
|
+
}
|
|
123
|
+
/** Host a local image/video as the browser's webcam feed. Same public-upload
|
|
124
|
+
* path as a fixture (so it is fetchable from the browser container and is
|
|
125
|
+
* deleted the same way), validated first. */
|
|
126
|
+
export async function uploadCameraFeed(projectGuid, localPath) {
|
|
127
|
+
assertCameraFile(localPath);
|
|
128
|
+
return uploadPublicFixture(projectGuid, localPath, '--camera');
|
|
129
|
+
}
|
|
41
130
|
//# sourceMappingURL=page-fixtures.js.map
|
package/dist/prefs.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Machine-level user preferences (~/.gipity/prefs.json) - the `gipity build`
|
|
3
|
+
* picker's memory. Deliberately NOT in a project's .gipity.json: per-project
|
|
4
|
+
* storage would re-ask the agent/model questions on every new project, which
|
|
5
|
+
* defeats "hit enter twice".
|
|
6
|
+
*
|
|
7
|
+
* Shape:
|
|
8
|
+
* {
|
|
9
|
+
* "lastAgent": "claude",
|
|
10
|
+
* "lastModel": { "claude": "opus", "codex": null } // per-agent so a
|
|
11
|
+
* } // Codex session never
|
|
12
|
+
* // clobbers the Claude pick
|
|
13
|
+
*
|
|
14
|
+
* A `lastModel` of null/absent means "Agent default" - launch passes no
|
|
15
|
+
* model flag and the agent's own model memory stays authoritative.
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
18
|
+
import { homedir } from 'os';
|
|
19
|
+
import { join } from 'path';
|
|
20
|
+
const PREFS_PATH = join(homedir(), '.gipity', 'prefs.json');
|
|
21
|
+
export function readPrefs() {
|
|
22
|
+
if (!existsSync(PREFS_PATH))
|
|
23
|
+
return {};
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(readFileSync(PREFS_PATH, 'utf-8'));
|
|
26
|
+
return {
|
|
27
|
+
lastAgent: typeof parsed.lastAgent === 'string' ? parsed.lastAgent : undefined,
|
|
28
|
+
lastModel: parsed.lastModel && typeof parsed.lastModel === 'object' ? parsed.lastModel : undefined,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return {}; // unreadable prefs must never block a launch
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function writePrefs(update) {
|
|
36
|
+
try {
|
|
37
|
+
const current = readPrefs();
|
|
38
|
+
const next = {
|
|
39
|
+
...current,
|
|
40
|
+
...update,
|
|
41
|
+
lastModel: { ...current.lastModel, ...update.lastModel },
|
|
42
|
+
};
|
|
43
|
+
mkdirSync(join(homedir(), '.gipity'), { recursive: true });
|
|
44
|
+
writeFileSync(PREFS_PATH, JSON.stringify(next, null, 2) + '\n');
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
/* best-effort - prefs are a convenience */
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=prefs.js.map
|
package/dist/project-setup.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { clearConfigCache, saveConfigAt, getApiBaseOverride, DEFAULT_API_BASE } from './config.js';
|
|
8
8
|
import { sync } from './sync.js';
|
|
9
9
|
import { createProgressReporter } from './progress.js';
|
|
10
|
-
import {
|
|
10
|
+
import { setupProjectTools, DEFAULT_TOOLS, DEFAULT_SYNC_IGNORE } from './setup.js';
|
|
11
11
|
import { substituteDir } from './template-vars.js';
|
|
12
12
|
import { muted } from './colors.js';
|
|
13
13
|
/** Write `.gipity.json` in `dir`, chdir into it so the hook/skill writers
|
|
@@ -61,15 +61,7 @@ export async function finalizeLocalProject(opts) {
|
|
|
61
61
|
throw err;
|
|
62
62
|
// soft mode - swallow; caller can log
|
|
63
63
|
}
|
|
64
|
-
|
|
65
|
-
// Claude hooks (file-sync + capture) only matter if the Claude primer is
|
|
66
|
-
// being written. Skipping them for Codex/Gemini/Cursor-only inits avoids
|
|
67
|
-
// littering `.claude/settings.json` into projects that don't use Claude.
|
|
68
|
-
if (tools.some(t => t.key === 'claude'))
|
|
69
|
-
setupClaudeHooks();
|
|
70
|
-
for (const t of tools)
|
|
71
|
-
t.setup();
|
|
72
|
-
setupGitignore();
|
|
64
|
+
setupProjectTools(opts.tools ?? DEFAULT_TOOLS);
|
|
73
65
|
return { applied };
|
|
74
66
|
}
|
|
75
67
|
//# sourceMappingURL=project-setup.js.map
|
package/dist/provider-docs.js
CHANGED
|
@@ -42,17 +42,17 @@ export const GEMINI_TTS_VOICES_DOC = `Zephyr, Puck, Charon, Kore, Fenrir, Leda,
|
|
|
42
42
|
export const GEMINI_TTS_VOICES_SHORT = `Kore, Puck, Zephyr, Charon, Fenrir, Leda, Orus, Aoede, and 22 more`;
|
|
43
43
|
export const IMAGE_GEMINI_ASPECT_RATIOS = `1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 4:5, 5:4, 21:9`;
|
|
44
44
|
export const IMAGE_GEMINI_SIZES = `512, 1K, 2K, 4K`;
|
|
45
|
-
export const IMAGE_MODELS_DOC = `openai: gpt-image-2
|
|
46
|
-
export const IMAGE_PROVIDERS_BULLET = `- **OpenAI**: \`gpt-image-2
|
|
47
|
-
- **BFL/Flux**: \`flux-2-pro, flux-2-flex, flux-2-max, flux-
|
|
48
|
-
- **Gemini/Nano Banana**: \`gemini-2.5-flash-image, gemini-3.1-flash-image, gemini-3-pro-image\``;
|
|
45
|
+
export const IMAGE_MODELS_DOC = `openai: gpt-image-2. bfl: flux-2-pro, flux-2-flex, flux-2-max, flux-2-klein-9b, flux-2-klein-4b. gemini: gemini-3.1-flash-lite-image, gemini-2.5-flash-image, gemini-3.1-flash-image, gemini-3-pro-image`;
|
|
46
|
+
export const IMAGE_PROVIDERS_BULLET = `- **OpenAI**: \`gpt-image-2\`
|
|
47
|
+
- **BFL/Flux**: \`flux-2-pro, flux-2-flex, flux-2-max, flux-2-klein-9b, flux-2-klein-4b\`
|
|
48
|
+
- **Gemini/Nano Banana**: \`gemini-3.1-flash-lite-image, gemini-2.5-flash-image, gemini-3.1-flash-image, gemini-3-pro-image\``;
|
|
49
49
|
export const IMAGE_PROVIDERS_LIST = `openai, bfl, gemini`;
|
|
50
50
|
export const IMAGE_PROVIDER_DESCRIPTIONS = {
|
|
51
|
-
'openai': `OpenAI (gpt-image-2
|
|
52
|
-
'bfl': `BFL/Flux (flux-2-pro, flux-2-flex, flux-2-max, flux-
|
|
53
|
-
'gemini': `Gemini/Nano Banana (gemini-2.5-flash-image, gemini-3.1-flash-image, gemini-3-pro-image)`,
|
|
51
|
+
'openai': `OpenAI (gpt-image-2)`,
|
|
52
|
+
'bfl': `BFL/Flux (flux-2-pro, flux-2-flex, flux-2-max, flux-2-klein-9b, flux-2-klein-4b)`,
|
|
53
|
+
'gemini': `Gemini/Nano Banana (gemini-3.1-flash-lite-image, gemini-2.5-flash-image, gemini-3.1-flash-image, gemini-3-pro-image)`,
|
|
54
54
|
};
|
|
55
|
-
export const LLM_DEFAULT_MODELS_DOC = `OpenAI: gpt-4
|
|
55
|
+
export const LLM_DEFAULT_MODELS_DOC = `OpenAI: gpt-5.4-nano (cheapest), gpt-5.4-mini (cheap reasoning). Anthropic: claude-haiku-4-5 (cheapest). Gemini: gemini-2.5-flash-lite (cheapest, 1M context)`;
|
|
56
56
|
export const LLM_PROVIDERS_LIST = `anthropic, openai, gemini`;
|
|
57
57
|
export const OPENAI_TTS_VOICES_DOC = `alloy, ash, ballad, coral, echo, fable, nova, onyx, sage, shimmer, verse`;
|
|
58
58
|
export const TRANSCRIBE_PROVIDERS_DOC = `elevenlabs (default, Scribe v2), openai (GPT-4o Transcribe), gemini (Gemini 2.5 Flash - cheapest, multilingual)`;
|
|
@@ -63,7 +63,7 @@ export const TTS_PROVIDER_DESCRIPTIONS = {
|
|
|
63
63
|
'gemini': `Gemini (30 voices: Kore, Puck, Zephyr, Charon, Fenrir, Leda, Orus, Aoede, and 22 more). Multi-speaker (up to 2) and 60+ languages`,
|
|
64
64
|
};
|
|
65
65
|
export const VIDEO_ASPECT_RATIOS = `16:9 (landscape), 9:16 (portrait), 1:1 (square)`;
|
|
66
|
-
export const VIDEO_MODELS_DOC = `veo-3.1-generate-preview (best quality, ~$0.40/sec), veo-3.1-fast-generate-preview (faster, ~$0.
|
|
67
|
-
export const VIDEO_MODELS_LIST = `veo-3.1-generate-preview, veo-3.1-fast-generate-preview, veo-3.1-lite-generate-preview`;
|
|
66
|
+
export const VIDEO_MODELS_DOC = `veo-3.1-generate-preview (best quality, up to 4K, ~$0.40/sec), veo-3.1-fast-generate-preview (faster, ~$0.10/sec), veo-3.1-lite-generate-preview (budget, ~$0.05/sec), gemini-omni-flash-preview (3-10s @ 720p, conversational editing, ~$0.10/sec)`;
|
|
67
|
+
export const VIDEO_MODELS_LIST = `veo-3.1-generate-preview, veo-3.1-fast-generate-preview, veo-3.1-lite-generate-preview, gemini-omni-flash-preview`;
|
|
68
68
|
export const VIDEO_RESOLUTIONS = `720p, 1080p, 4k`;
|
|
69
69
|
//# sourceMappingURL=provider-docs.js.map
|
package/dist/relay/daemon.js
CHANGED
|
@@ -6,12 +6,13 @@ import { homedir, hostname, platform as osPlatform, loadavg, freemem, totalmem,
|
|
|
6
6
|
import { join } from 'path';
|
|
7
7
|
import { getApiBaseOverride, DEFAULT_API_BASE } from '../config.js';
|
|
8
8
|
import { getProjectsRoot } from './paths.js';
|
|
9
|
-
import {
|
|
9
|
+
import { setupProjectTools, DEFAULT_SYNC_IGNORE } from '../setup.js';
|
|
10
10
|
import { getAuth, readAuthFresh, refreshTokenIfNeeded, accessTokenExpired } from '../auth.js';
|
|
11
11
|
import { post } from '../api.js';
|
|
12
12
|
import * as state from './state.js';
|
|
13
13
|
import { createLineSplitter, parseEvent, mapEventToEntries, } from './stream-json.js';
|
|
14
14
|
import { IngestQueue } from './ingest-queue.js';
|
|
15
|
+
import { ImageBlockRewriter } from './media-upload.js';
|
|
15
16
|
import { DeltaAccumulator, DeltaBatcher } from './stream-delta.js';
|
|
16
17
|
import { randomUUID } from 'crypto';
|
|
17
18
|
import { deviceFetch, bridgeAbort as bridgeAbortImpl } from './device-http.js';
|
|
@@ -23,7 +24,8 @@ import { SessionPool, PoolFullError } from './session-pool.js';
|
|
|
23
24
|
import { getConfig } from '../config.js';
|
|
24
25
|
import { getAccountSlug } from '../api.js';
|
|
25
26
|
import { buildFreshWrap, buildResumeWrap, buildProjectContextBlock } from '../prompts.js';
|
|
26
|
-
import { fetchProjectStats } from '../commands/
|
|
27
|
+
import { fetchProjectStats } from '../commands/build.js';
|
|
28
|
+
import { getAdapterBySource } from '../agents/index.js';
|
|
27
29
|
// Re-exported so the existing `relay-bridge-abort.test.ts` keeps working.
|
|
28
30
|
// New callers should import from device-http.js directly.
|
|
29
31
|
export const bridgeAbort = bridgeAbortImpl;
|
|
@@ -875,7 +877,13 @@ async function handleDispatch(claimed) {
|
|
|
875
877
|
// retry instead of the old fire-and-forget loss on network errors.
|
|
876
878
|
// Daemon-authored entries get a random source_uuid so retried batches
|
|
877
879
|
// dedup server-side.
|
|
878
|
-
|
|
880
|
+
// The rewriter uploads any base64 image blocks (Read-of-screenshot
|
|
881
|
+
// results) to VFS and swaps in image_ref blocks before the batch posts —
|
|
882
|
+
// transcripts carry URLs, never image bytes. Retries re-enter it, which
|
|
883
|
+
// is safe: rewritten entries have nothing left to upload, and the server
|
|
884
|
+
// stores by content hash so a replayed upload dedups.
|
|
885
|
+
const rewriter = new ImageBlockRewriter(d.conversation_guid, (msg, meta) => log('warn', msg, { id: d.short_guid, ...meta }));
|
|
886
|
+
const queue = new IngestQueue(async (entries) => postIngest(d.conversation_guid, await rewriter.rewrite(entries)), { onWarn: (msg, meta) => log('warn', msg, { id: d.short_guid, ...meta }) });
|
|
879
887
|
const pushSystem = (content) => {
|
|
880
888
|
queue.push({ kind: 'system', content, ts: new Date().toISOString(), source_uuid: randomUUID() });
|
|
881
889
|
};
|
|
@@ -886,6 +894,9 @@ async function handleDispatch(claimed) {
|
|
|
886
894
|
let bootstrapped;
|
|
887
895
|
try {
|
|
888
896
|
({ cwd, bootstrapped } = await resolveCwdForProject(d));
|
|
897
|
+
// Lets the rewriter map an absolute Read path (…/screenshots/x.png)
|
|
898
|
+
// to its project-relative VFS path for content-hash dedup.
|
|
899
|
+
rewriter.setCwd(cwd);
|
|
889
900
|
log('debug', 'resolved project cwd', { id: d.short_guid, project: d.project_slug, cwd, bootstrapped });
|
|
890
901
|
}
|
|
891
902
|
catch (err) {
|
|
@@ -939,6 +950,15 @@ async function handleDispatch(claimed) {
|
|
|
939
950
|
if (!syncFailed)
|
|
940
951
|
pushSystem(bootstrapped ? 'Project files synced.' : 'Attached files synced.');
|
|
941
952
|
}
|
|
953
|
+
// Which agent runs this dispatch. Older servers omit remote_type - they
|
|
954
|
+
// only ever dispatched Claude Code.
|
|
955
|
+
const adapter = getAdapterBySource(d.remote_type ?? 'claude_code');
|
|
956
|
+
// Non-Claude agents take the simpler hook-capture dispatch path: no
|
|
957
|
+
// stream-json ingest, no session pool.
|
|
958
|
+
if (adapter.key !== 'claude') {
|
|
959
|
+
await handleNonClaudeDispatch(adapter, d, cwd, queue, pushSystem, flushQueue);
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
942
962
|
// Phase 2: if the session pool is enabled, try to run this turn in a
|
|
943
963
|
// long-lived Claude Code session (fast follow-up + clean interrupt). Any
|
|
944
964
|
// failure - pool saturated, SDK error, wrap failure - falls through to the
|
|
@@ -948,8 +968,11 @@ async function handleDispatch(claimed) {
|
|
|
948
968
|
if (handled)
|
|
949
969
|
return;
|
|
950
970
|
}
|
|
951
|
-
// Build argv for `gipity claude -p …` (or with --resume).
|
|
952
|
-
// as array so the message string can't be interpreted as
|
|
971
|
+
// Build argv for `gipity build --agent claude -p …` (or with --resume).
|
|
972
|
+
// No shell - argv as array so the message string can't be interpreted as
|
|
973
|
+
// shell syntax. (`build` is the renamed launcher; the flags below pass
|
|
974
|
+
// through it to the `claude` binary unchanged, exactly as the legacy
|
|
975
|
+
// `gipity claude -p` spawn did.)
|
|
953
976
|
//
|
|
954
977
|
// `--permission-mode bypassPermissions`: a relay dispatch has no
|
|
955
978
|
// human on the other end to click "Approve" - Claude prompting would
|
|
@@ -957,7 +980,7 @@ async function handleDispatch(claimed) {
|
|
|
957
980
|
// the device and dispatching the message; skipping the interactive
|
|
958
981
|
// prompt is correct (same authority as running `claude -p` in a local
|
|
959
982
|
// terminal yourself).
|
|
960
|
-
const args = ['claude', '-p', d.message, '--permission-mode', 'bypassPermissions'];
|
|
983
|
+
const args = ['build', '--agent', 'claude', '-p', d.message, '--permission-mode', 'bypassPermissions'];
|
|
961
984
|
// Relay sessions run in -p mode, where Claude Code's AskUserQuestion tool
|
|
962
985
|
// is unavailable - so without help the model would ask clarifying
|
|
963
986
|
// questions as prose the user just types back. This system-prompt
|
|
@@ -974,7 +997,7 @@ async function handleDispatch(claimed) {
|
|
|
974
997
|
if (d.kind === 'resume' && d.remote_session_id) {
|
|
975
998
|
args.push('--resume', d.remote_session_id);
|
|
976
999
|
}
|
|
977
|
-
log('debug', 'spawning gipity claude', {
|
|
1000
|
+
log('debug', 'spawning gipity build --agent claude', {
|
|
978
1001
|
id: d.short_guid,
|
|
979
1002
|
cwd,
|
|
980
1003
|
args,
|
|
@@ -1113,7 +1136,92 @@ async function handleDispatch(claimed) {
|
|
|
1113
1136
|
}
|
|
1114
1137
|
else {
|
|
1115
1138
|
log('warn', 'dispatch child exited nonzero', { id: d.short_guid, exitCode, ms });
|
|
1116
|
-
await ack(d.short_guid, 'error', `
|
|
1139
|
+
await ack(d.short_guid, 'error', `Claude Code exited with code ${exitCode}${stderrNote}`);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Dispatch for non-Claude agents (Codex, Grok). Deliberately simpler than
|
|
1144
|
+
* the Claude path: there is no stream-json ingest mapper for these agents -
|
|
1145
|
+
* hook-based session capture (Phase C) owns the transcript. The spawned
|
|
1146
|
+
* `gipity build --agent <key> -p …` child gets GIPITY_CONVERSATION_GUID (so
|
|
1147
|
+
* capture posts into the right conversation) and capture is NOT turned off.
|
|
1148
|
+
* The daemon contributes: the "Running <agent>" marker, byte-level progress
|
|
1149
|
+
* heartbeats (spawnGipityClaude's generic plumbing with streamJson:false),
|
|
1150
|
+
* post-run file sync, the tail marker, and the ack. The prompt entry itself
|
|
1151
|
+
* arrives via capture from the agent's own transcript - the daemon doesn't
|
|
1152
|
+
* push one, or it would render twice.
|
|
1153
|
+
*/
|
|
1154
|
+
async function handleNonClaudeDispatch(adapter, d, cwd, queue, pushSystem, flushQueue) {
|
|
1155
|
+
// `gipity build` translates these into the agent's own argv via the same
|
|
1156
|
+
// adapter (Codex: `exec …`; Grok: `-p … --always-approve`). Raw message -
|
|
1157
|
+
// AGENTS.md carries the Gipity context for these agents; no wrapping.
|
|
1158
|
+
const args = ['build', '--agent', adapter.key, '-p', d.message, '--bypass-approvals'];
|
|
1159
|
+
if (d.model)
|
|
1160
|
+
args.push('--model', d.model);
|
|
1161
|
+
if (d.kind === 'resume' && d.remote_session_id)
|
|
1162
|
+
args.push('--resume', d.remote_session_id);
|
|
1163
|
+
log('debug', `spawning gipity build --agent ${adapter.key}`, {
|
|
1164
|
+
id: d.short_guid, cwd, args, conv: d.conversation_guid,
|
|
1165
|
+
chain: d.kind === 'resume' ? `resume ${d.remote_session_id}` : 'start (fresh session)',
|
|
1166
|
+
});
|
|
1167
|
+
const words = d.message.trim().split(/\s+/).filter(Boolean).length;
|
|
1168
|
+
pushSystem(`Running ${adapter.displayName} - ${words.toLocaleString('en-US')} words`);
|
|
1169
|
+
const t0 = Date.now();
|
|
1170
|
+
let exitCode = 1;
|
|
1171
|
+
let spawnErr = null;
|
|
1172
|
+
let killed = false;
|
|
1173
|
+
let runtimeLimit = false;
|
|
1174
|
+
let stderrTail = '';
|
|
1175
|
+
try {
|
|
1176
|
+
const result = await spawnGipityClaude(args, cwd, d, queue, { streamJson: false });
|
|
1177
|
+
exitCode = result.exitCode;
|
|
1178
|
+
killed = result.killed;
|
|
1179
|
+
runtimeLimit = result.runtimeLimit ?? false;
|
|
1180
|
+
stderrTail = result.stderrTail ?? '';
|
|
1181
|
+
}
|
|
1182
|
+
catch (err) {
|
|
1183
|
+
spawnErr = err?.message || String(err);
|
|
1184
|
+
log('error', 'dispatch spawn failed', { id: d.short_guid, err: spawnErr });
|
|
1185
|
+
}
|
|
1186
|
+
const dur = formatDuration(Date.now() - t0);
|
|
1187
|
+
// Same post-run reconcile as the Claude path: push files the agent wrote
|
|
1188
|
+
// via Bash/scripts back to VFS before the ack (hooks only cover its
|
|
1189
|
+
// native edit tools). Skipped on kill - the replacement dispatch owns the tree.
|
|
1190
|
+
if (!spawnErr && !killed) {
|
|
1191
|
+
try {
|
|
1192
|
+
await spawnSync(cwd, PROJECT_SYNC_TIMEOUT_MS);
|
|
1193
|
+
}
|
|
1194
|
+
catch (err) {
|
|
1195
|
+
log('warn', 'sync after dispatch failed', { id: d.short_guid, err: err?.message });
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
const stderrNote = stderrTail ? `: ${stderrTail.slice(0, 300)}` : '';
|
|
1199
|
+
const tail = runtimeLimit
|
|
1200
|
+
? `stopped after ${dur} (runtime limit)`
|
|
1201
|
+
: killed
|
|
1202
|
+
? `cancelled (${dur})`
|
|
1203
|
+
: spawnErr
|
|
1204
|
+
? `failed (${dur}: ${spawnErr})`
|
|
1205
|
+
: exitCode === 0
|
|
1206
|
+
? `finished (${dur})`
|
|
1207
|
+
: `failed (${dur}, exit ${exitCode}${stderrNote})`;
|
|
1208
|
+
pushSystem(`${adapter.displayName} ${tail}`);
|
|
1209
|
+
await flushQueue();
|
|
1210
|
+
if (runtimeLimit) {
|
|
1211
|
+
await ack(d.short_guid, 'error', `${adapter.displayName} stopped after ${dur} (runtime limit)`);
|
|
1212
|
+
}
|
|
1213
|
+
else if (killed) {
|
|
1214
|
+
await ack(d.short_guid, 'cancelled');
|
|
1215
|
+
}
|
|
1216
|
+
else if (spawnErr) {
|
|
1217
|
+
await ack(d.short_guid, 'error', spawnErr);
|
|
1218
|
+
}
|
|
1219
|
+
else if (exitCode === 0) {
|
|
1220
|
+
log('info', 'dispatch done', { id: d.short_guid, agent: adapter.key });
|
|
1221
|
+
await ack(d.short_guid, 'done');
|
|
1222
|
+
}
|
|
1223
|
+
else {
|
|
1224
|
+
await ack(d.short_guid, 'error', `${adapter.displayName} exited with code ${exitCode}${stderrNote}`);
|
|
1117
1225
|
}
|
|
1118
1226
|
}
|
|
1119
1227
|
/**
|
|
@@ -1362,10 +1470,7 @@ async function resolveCwdForProject(d) {
|
|
|
1362
1470
|
const origCwd = process.cwd();
|
|
1363
1471
|
try {
|
|
1364
1472
|
process.chdir(path);
|
|
1365
|
-
|
|
1366
|
-
setupClaudeMd();
|
|
1367
|
-
setupAgentsMd();
|
|
1368
|
-
setupGitignore();
|
|
1473
|
+
setupProjectTools();
|
|
1369
1474
|
}
|
|
1370
1475
|
finally {
|
|
1371
1476
|
process.chdir(origCwd);
|
|
@@ -1552,6 +1657,11 @@ async function spawnSync(cwd, timeoutMs, onSpawn) {
|
|
|
1552
1657
|
});
|
|
1553
1658
|
}
|
|
1554
1659
|
export async function spawnGipityClaude(args, cwd, d, queue, meta) {
|
|
1660
|
+
// streamJson (default true) = the Claude path: parse the child's
|
|
1661
|
+
// stream-json stdout into ingest entries and stand hook capture down.
|
|
1662
|
+
// false = non-Claude agents (Codex/Grok): hook capture owns the
|
|
1663
|
+
// transcript; stdout only feeds the byte-level progress heartbeat.
|
|
1664
|
+
const streamJson = meta?.streamJson !== false;
|
|
1555
1665
|
// resolveCommand: on Windows the bare `gipity` is a .cmd shim that spawn
|
|
1556
1666
|
// can't launch without an explicit path. An explicit env override is used
|
|
1557
1667
|
// verbatim (it may be a full path); only the default name is resolved.
|
|
@@ -1562,13 +1672,19 @@ export async function spawnGipityClaude(args, cwd, d, queue, meta) {
|
|
|
1562
1672
|
// `--include-partial-messages` adds stream_event token deltas, which
|
|
1563
1673
|
// feed the ephemeral live-typing channel (see stream-delta.ts); whole
|
|
1564
1674
|
// assistant/tool events still arrive unchanged for the persistent path.
|
|
1565
|
-
const fullArgs =
|
|
1675
|
+
const fullArgs = streamJson
|
|
1676
|
+
? [...args, '--output-format', 'stream-json', '--verbose', '--include-partial-messages']
|
|
1677
|
+
: [...args];
|
|
1566
1678
|
// GIPITY_CAPTURE=off: the daemon owns capture for this dispatch (it
|
|
1567
1679
|
// parses the stream-json on stdout), so the plugin's lifecycle-hook
|
|
1568
1680
|
// capture must stand down. `gipity claude` sets the same sentinel on the
|
|
1569
1681
|
// Claude child when it detects a daemon spawn; setting it here too keeps
|
|
1570
|
-
// dispatches double-post-free even across CLI version skew.
|
|
1571
|
-
|
|
1682
|
+
// dispatches double-post-free even across CLI version skew. Non-Claude
|
|
1683
|
+
// agents are the opposite: hook capture IS the transcript path, so the
|
|
1684
|
+
// sentinel must NOT be set.
|
|
1685
|
+
const env = streamJson
|
|
1686
|
+
? childEnv({ GIPITY_CONVERSATION_GUID: d.conversation_guid, GIPITY_CAPTURE: 'off' })
|
|
1687
|
+
: childEnv({ GIPITY_CONVERSATION_GUID: d.conversation_guid });
|
|
1572
1688
|
return new Promise((resolve, reject) => {
|
|
1573
1689
|
const child = spawnCommand(cmd, fullArgs, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
1574
1690
|
// `exited` fires when the child fully unwinds (exit event). Callers
|
|
@@ -1583,7 +1699,11 @@ export async function spawnGipityClaude(args, cwd, d, queue, meta) {
|
|
|
1583
1699
|
// no longer drops stream content permanently (source_uuid dedup makes
|
|
1584
1700
|
// the retries safe). Falls back to a local queue when the caller
|
|
1585
1701
|
// didn't pass one (tests, direct invocation).
|
|
1586
|
-
const q = queue ??
|
|
1702
|
+
const q = queue ?? (() => {
|
|
1703
|
+
const rw = new ImageBlockRewriter(d.conversation_guid, (msg, meta) => log('warn', msg, { id: d.short_guid, ...meta }));
|
|
1704
|
+
rw.setCwd(cwd);
|
|
1705
|
+
return new IngestQueue(async (entries) => postIngest(d.conversation_guid, await rw.rewrite(entries)), { onWarn: (msg, meta) => log('warn', msg, { id: d.short_guid, ...meta }) });
|
|
1706
|
+
})();
|
|
1587
1707
|
const ownQueue = !queue;
|
|
1588
1708
|
// Progress heartbeat state. Measured at the daemon boundary - this is
|
|
1589
1709
|
// what we actually observe, not anything the child self-reports. These
|
|
@@ -1766,9 +1886,11 @@ export async function spawnGipityClaude(args, cwd, d, queue, meta) {
|
|
|
1766
1886
|
child.stdout?.on('data', (chunk) => {
|
|
1767
1887
|
stdoutBytesTotal += chunk.length;
|
|
1768
1888
|
lastStdoutByteAt = Date.now();
|
|
1769
|
-
|
|
1889
|
+
if (streamJson)
|
|
1890
|
+
splitter.push(chunk);
|
|
1770
1891
|
});
|
|
1771
|
-
child.stdout?.on('end', () =>
|
|
1892
|
+
child.stdout?.on('end', () => { if (streamJson)
|
|
1893
|
+
splitter.flush(); });
|
|
1772
1894
|
// Stderr: human-readable only (Claude's progress bars, errors).
|
|
1773
1895
|
// Kept on the daemon's own stderr for `gipity relay log`, plus a
|
|
1774
1896
|
// small tail ring so a crash with no stream output can include the
|
|
@@ -31,6 +31,28 @@ export function bridgeAbort(outer, inner) {
|
|
|
31
31
|
outer.addEventListener('abort', onAbort, { once: true });
|
|
32
32
|
return () => outer.removeEventListener('abort', onAbort);
|
|
33
33
|
}
|
|
34
|
+
/** Device-auth binary POST — raw bytes body (no JSON/base64 wrapping).
|
|
35
|
+
* Used by the transcript-media upload, where the whole point is that the
|
|
36
|
+
* image travels as bytes instead of base64. */
|
|
37
|
+
export async function deviceFetchBinary(method, path, body, contentType, timeoutMs) {
|
|
38
|
+
const controller = timeoutMs ? new AbortController() : null;
|
|
39
|
+
const timer = controller ? setTimeout(() => controller.abort('timeout'), timeoutMs) : null;
|
|
40
|
+
try {
|
|
41
|
+
return await fetch(`${apiBase()}${path}`, {
|
|
42
|
+
method,
|
|
43
|
+
headers: {
|
|
44
|
+
'Authorization': `Bearer ${deviceToken()}`,
|
|
45
|
+
'Content-Type': contentType,
|
|
46
|
+
},
|
|
47
|
+
body: new Uint8Array(body),
|
|
48
|
+
signal: controller?.signal,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
if (timer)
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
34
56
|
/** Device-auth HTTP helper. Sends `Authorization: Bearer <device-token>`
|
|
35
57
|
* + `Content-Type: application/json` to the configured API base. */
|
|
36
58
|
export async function deviceFetch(method, path, body, timeoutMs, signal) {
|
|
@@ -137,14 +137,16 @@ export async function collectDiagnostics() {
|
|
|
137
137
|
} })();
|
|
138
138
|
// Run the subprocess probes concurrently (each already bounded + best-effort)
|
|
139
139
|
// so the whole snapshot costs one timeout, not the sum of four.
|
|
140
|
-
const [claude, codex, cursor, gpu] = await Promise.all([
|
|
141
|
-
probeVersion('claude'), probeVersion('codex'), probeVersion('cursor'), detectGpu(),
|
|
140
|
+
const [claude, codex, grok, cursor, gpu] = await Promise.all([
|
|
141
|
+
probeVersion('claude'), probeVersion('codex'), probeVersion('grok'), probeVersion('cursor'), detectGpu(),
|
|
142
142
|
]);
|
|
143
143
|
const agents = {};
|
|
144
144
|
if (claude)
|
|
145
145
|
agents.claude_code = claude;
|
|
146
146
|
if (codex)
|
|
147
147
|
agents.codex = codex;
|
|
148
|
+
if (grok)
|
|
149
|
+
agents.grok = grok;
|
|
148
150
|
if (cursor)
|
|
149
151
|
agents.cursor = cursor;
|
|
150
152
|
return {
|