gipity 1.0.428 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/claude-code.js +53 -0
- package/dist/agents/codex.js +49 -0
- package/dist/agents/grok.js +54 -0
- package/dist/agents/index.js +23 -0
- package/dist/agents/types.js +18 -0
- package/dist/api.js +7 -0
- package/dist/capture/sources/codex.js +216 -0
- package/dist/capture/sources/grok.js +178 -0
- package/dist/client-context.js +2 -0
- package/dist/commands/build.js +1352 -0
- package/dist/commands/chat.js +9 -3
- package/dist/commands/claude.js +4 -13
- package/dist/commands/db.js +12 -5
- package/dist/commands/deploy.js +19 -1
- package/dist/commands/doctor.js +8 -2
- package/dist/commands/generate.js +61 -4
- package/dist/commands/init.js +9 -15
- package/dist/commands/page-eval.js +404 -56
- package/dist/commands/page-inspect.js +15 -5
- package/dist/commands/page-screenshot.js +269 -64
- package/dist/commands/project.js +1 -1
- package/dist/commands/relay-install.js +1 -1
- package/dist/commands/relay.js +2 -2
- package/dist/commands/sandbox.js +62 -16
- package/dist/commands/setup.js +16 -10
- package/dist/commands/uninstall.js +25 -3
- package/dist/hooks/capture-runner.js +136 -39
- package/dist/index.js +1962 -668
- package/dist/knowledge.js +3 -3
- package/dist/page-fixtures.js +92 -3
- package/dist/prefs.js +50 -0
- package/dist/project-setup.js +2 -10
- package/dist/provider-docs.js +10 -10
- package/dist/relay/daemon.js +140 -18
- package/dist/relay/device-http.js +22 -0
- package/dist/relay/diagnostics.js +4 -2
- package/dist/relay/media-upload.js +184 -0
- package/dist/relay/onboarding.js +2 -2
- package/dist/setup.js +262 -18
- package/package.json +4 -3
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcript image rewrite — the relay's "no inline base64" chokepoint.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code's `Read` of an image file (screenshots, generated art, …)
|
|
5
|
+
* returns the bytes as a `{type:'image', source:{type:'base64', data}}`
|
|
6
|
+
* block inside the tool_result. Shipping that through ingest bloats the
|
|
7
|
+
* transcript DB and, worse, blows the server's 200 KB per-entry cap so
|
|
8
|
+
* real screenshots silently never reach the web client at all.
|
|
9
|
+
*
|
|
10
|
+
* `ImageBlockRewriter` sits in front of the ingest POST: it uploads each
|
|
11
|
+
* base64 image block's bytes to `POST /remote-sessions/:convGuid/media`
|
|
12
|
+
* (raw binary body) and swaps the block for a small `image_ref` pointing
|
|
13
|
+
* at the stored VFS file. The server dedups by content hash — a `Read` of
|
|
14
|
+
* an already-synced screenshot references the existing node.
|
|
15
|
+
*
|
|
16
|
+
* Failure handling: an upload that fails leaves small images inline
|
|
17
|
+
* (status-quo rendering still works) but replaces oversize ones with a
|
|
18
|
+
* text stub so one bad image can't poison the whole ingest batch. The
|
|
19
|
+
* IngestQueue retries a failed batch through this rewriter again, and the
|
|
20
|
+
* server's content-addressed storage makes replayed uploads idempotent.
|
|
21
|
+
*/
|
|
22
|
+
import path from 'path';
|
|
23
|
+
import { deviceFetchBinary } from './device-http.js';
|
|
24
|
+
/** Below this the upload round-trip outweighs the base64: tiny images
|
|
25
|
+
* (icons, favicons) stay inline. */
|
|
26
|
+
const MIN_UPLOAD_BYTES = 4 * 1024;
|
|
27
|
+
/** Server-side cap (TRANSCRIPT_MEDIA_MAX_BYTES). Larger images can't be
|
|
28
|
+
* stored; they get stubbed rather than shipped. */
|
|
29
|
+
const MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
|
|
30
|
+
/** Base64 payloads above this would break the server's 200 KB tool_result
|
|
31
|
+
* cap if left inline — when the upload fails, stub instead of poisoning
|
|
32
|
+
* the batch. Leaves headroom for the rest of the tool_result JSON. */
|
|
33
|
+
const INLINE_SAFE_B64_CHARS = 120_000;
|
|
34
|
+
/** How many tool_use → file_path pairs to remember for captioning/dedup. */
|
|
35
|
+
const PATH_MAP_CAP = 500;
|
|
36
|
+
/** Default uploader: raw-binary POST to the transcript-media endpoint. */
|
|
37
|
+
export const uploadViaDevice = async (convGuid, buf, opts) => {
|
|
38
|
+
const qs = new URLSearchParams({ filename: opts.filename, media_type: opts.mediaType });
|
|
39
|
+
if (opts.suggestedPath)
|
|
40
|
+
qs.set('suggested_path', opts.suggestedPath);
|
|
41
|
+
const res = await deviceFetchBinary('POST', `/remote-sessions/${encodeURIComponent(convGuid)}/media?${qs.toString()}`, buf, opts.mediaType, 30_000);
|
|
42
|
+
if (!res.ok) {
|
|
43
|
+
const body = await res.text().catch(() => '');
|
|
44
|
+
throw new Error(`HTTP ${res.status} ${body.slice(0, 120)}`);
|
|
45
|
+
}
|
|
46
|
+
const json = await res.json();
|
|
47
|
+
return json.data;
|
|
48
|
+
};
|
|
49
|
+
function extForMime(mime) {
|
|
50
|
+
switch (mime) {
|
|
51
|
+
case 'image/jpeg': return '.jpg';
|
|
52
|
+
case 'image/gif': return '.gif';
|
|
53
|
+
case 'image/webp': return '.webp';
|
|
54
|
+
case 'image/svg+xml': return '.svg';
|
|
55
|
+
default: return '.png';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function isBase64ImageBlock(b) {
|
|
59
|
+
return b?.type === 'image'
|
|
60
|
+
&& b.source?.type === 'base64'
|
|
61
|
+
&& typeof b.source.data === 'string'
|
|
62
|
+
&& b.source.data.length > 0;
|
|
63
|
+
}
|
|
64
|
+
export class ImageBlockRewriter {
|
|
65
|
+
convGuid;
|
|
66
|
+
onWarn;
|
|
67
|
+
upload;
|
|
68
|
+
/** tool_use_id → the file_path the tool was asked to read (for naming +
|
|
69
|
+
* server-side dedup against the synced node at that path). */
|
|
70
|
+
pathsByToolUseId = new Map();
|
|
71
|
+
/** Project root on this machine, once the dispatch resolves it — lets an
|
|
72
|
+
* absolute Read path map to its project-relative VFS path. */
|
|
73
|
+
cwd = null;
|
|
74
|
+
constructor(convGuid, onWarn, upload = uploadViaDevice) {
|
|
75
|
+
this.convGuid = convGuid;
|
|
76
|
+
this.onWarn = onWarn;
|
|
77
|
+
this.upload = upload;
|
|
78
|
+
}
|
|
79
|
+
setCwd(cwd) {
|
|
80
|
+
this.cwd = cwd;
|
|
81
|
+
}
|
|
82
|
+
/** Rewrite every base64 image block in the batch to an image_ref. Never
|
|
83
|
+
* throws — per-image failures degrade per the policy above. */
|
|
84
|
+
async rewrite(entries) {
|
|
85
|
+
const out = [];
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
if (entry.kind === 'tool_use') {
|
|
88
|
+
this.recordToolPath(entry);
|
|
89
|
+
out.push(entry);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (entry.kind !== 'tool_result' || !Array.isArray(entry.content) || typeof entry.tool_use_id !== 'string') {
|
|
93
|
+
out.push(entry);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const blocks = [];
|
|
97
|
+
for (const block of entry.content) {
|
|
98
|
+
blocks.push(isBase64ImageBlock(block)
|
|
99
|
+
? await this.rewriteBlock(block, entry.tool_use_id)
|
|
100
|
+
: block);
|
|
101
|
+
}
|
|
102
|
+
out.push({ ...entry, content: blocks });
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
recordToolPath(entry) {
|
|
107
|
+
if (typeof entry.tool_use_id !== 'string')
|
|
108
|
+
return;
|
|
109
|
+
const input = entry.tool_input;
|
|
110
|
+
const p = input && typeof input === 'object'
|
|
111
|
+
? (input.file_path ?? input.path ?? input.notebook_path)
|
|
112
|
+
: undefined;
|
|
113
|
+
if (typeof p !== 'string' || !p)
|
|
114
|
+
return;
|
|
115
|
+
if (this.pathsByToolUseId.size >= PATH_MAP_CAP) {
|
|
116
|
+
const oldest = this.pathsByToolUseId.keys().next().value;
|
|
117
|
+
if (oldest !== undefined)
|
|
118
|
+
this.pathsByToolUseId.delete(oldest);
|
|
119
|
+
}
|
|
120
|
+
this.pathsByToolUseId.set(entry.tool_use_id, p);
|
|
121
|
+
}
|
|
122
|
+
async rewriteBlock(block, toolUseId) {
|
|
123
|
+
const b64 = block.source.data;
|
|
124
|
+
let buf;
|
|
125
|
+
try {
|
|
126
|
+
buf = Buffer.from(b64, 'base64');
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return block; // not decodable — leave as-is
|
|
130
|
+
}
|
|
131
|
+
if (buf.length < MIN_UPLOAD_BYTES)
|
|
132
|
+
return block;
|
|
133
|
+
const mediaType = block.source.media_type || 'image/png';
|
|
134
|
+
const sourcePath = this.pathsByToolUseId.get(toolUseId);
|
|
135
|
+
const filename = sourcePath ? path.basename(sourcePath) : `image${extForMime(mediaType)}`;
|
|
136
|
+
const suggested = this.projectRelative(sourcePath);
|
|
137
|
+
if (buf.length > MAX_UPLOAD_BYTES) {
|
|
138
|
+
this.onWarn?.('transcript image exceeds upload cap - stubbed', { bytes: buf.length, filename });
|
|
139
|
+
return this.stub(filename, buf.length, 'too large to store');
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const ref = await this.upload(this.convGuid, buf, {
|
|
143
|
+
filename, mediaType, ...(suggested ? { suggestedPath: suggested } : {}),
|
|
144
|
+
});
|
|
145
|
+
return {
|
|
146
|
+
type: 'image_ref',
|
|
147
|
+
url: ref.url,
|
|
148
|
+
...(ref.thumb_url ? { thumb_url: ref.thumb_url } : {}),
|
|
149
|
+
media_type: mediaType,
|
|
150
|
+
path: ref.path,
|
|
151
|
+
...(ref.width != null ? { width: ref.width } : {}),
|
|
152
|
+
...(ref.height != null ? { height: ref.height } : {}),
|
|
153
|
+
bytes: ref.bytes,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
this.onWarn?.('transcript image upload failed', { filename, err: err?.message });
|
|
158
|
+
// Small enough to survive the ingest cap inline → keep the original
|
|
159
|
+
// (renders exactly as before this feature). Oversize → stub, because
|
|
160
|
+
// shipping it would 400 the entire batch and lose sibling entries.
|
|
161
|
+
if (b64.length <= INLINE_SAFE_B64_CHARS)
|
|
162
|
+
return block;
|
|
163
|
+
return this.stub(filename, buf.length, `upload failed: ${err?.message || 'unknown error'}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
stub(filename, bytes, reason) {
|
|
167
|
+
return {
|
|
168
|
+
type: 'text',
|
|
169
|
+
text: `[image ${filename} (${Math.round(bytes / 1024)} KB) omitted: ${reason}]`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/** Map an absolute local path to its project-relative VFS path, when it
|
|
173
|
+
* falls inside the dispatch's project root. */
|
|
174
|
+
projectRelative(p) {
|
|
175
|
+
if (!p || !this.cwd)
|
|
176
|
+
return undefined;
|
|
177
|
+
const abs = path.resolve(p);
|
|
178
|
+
const root = path.resolve(this.cwd);
|
|
179
|
+
if (!abs.startsWith(root + path.sep))
|
|
180
|
+
return undefined;
|
|
181
|
+
return abs.slice(root.length + 1).split(path.sep).join('/');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=media-upload.js.map
|
package/dist/relay/onboarding.js
CHANGED
|
@@ -129,7 +129,7 @@ export async function runRelaySetup(opts) {
|
|
|
129
129
|
return false;
|
|
130
130
|
}
|
|
131
131
|
// Start the daemon for this session.
|
|
132
|
-
const startNow = await confirm(' Start the relay now (and on future `gipity
|
|
132
|
+
const startNow = await confirm(' Start the relay now (and on future `gipity build` runs)?', { default: 'yes' });
|
|
133
133
|
if (startNow) {
|
|
134
134
|
startDaemon();
|
|
135
135
|
}
|
|
@@ -145,7 +145,7 @@ export async function runRelaySetup(opts) {
|
|
|
145
145
|
// generic non-zero. The relay itself is unaffected (it's already
|
|
146
146
|
// running this session, and `gipity claude` restarts it).
|
|
147
147
|
console.log(` ${muted('Auto-start needs systemd, which this WSL distro has off. The relay still runs -')}`);
|
|
148
|
-
console.log(` ${muted('it started just now and `gipity
|
|
148
|
+
console.log(` ${muted('it started just now and `gipity build` restarts it. For boot-time auto-start:')}`);
|
|
149
149
|
console.log(` ${muted('add [boot] systemd=true to /etc/wsl.conf, restart WSL, then run')} ${brand('gipity relay install')}${muted('.')}`);
|
|
150
150
|
}
|
|
151
151
|
else {
|
package/dist/setup.js
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
* Shared project setup helpers used by both `init` and `claude`.
|
|
3
3
|
*/
|
|
4
4
|
import { resolve, join, dirname } from 'path';
|
|
5
|
-
import { homedir } from 'os';
|
|
6
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
5
|
+
import { homedir, tmpdir } from 'os';
|
|
6
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, mkdtempSync, rmSync, cpSync, readdirSync } from 'fs';
|
|
7
7
|
import { resolveCommand, spawnSyncCommand } from './platform.js';
|
|
8
8
|
import { SKILLS_CONTENT, BUILD_VS_NON_BUILD_RULE, DEFINITION_OF_DONE } from './knowledge.js';
|
|
9
|
+
import { DEFAULT_API_BASE, resolveApiBase } from './config.js';
|
|
9
10
|
export { SKILLS_CONTENT };
|
|
10
11
|
/** Canonical list of workstation artifacts that are NOT part of the project.
|
|
11
12
|
* Used as the single source of truth for three separate decisions:
|
|
@@ -30,6 +31,7 @@ export { SKILLS_CONTENT };
|
|
|
30
31
|
export const PRIMER_FILES = {
|
|
31
32
|
claude: 'CLAUDE.md',
|
|
32
33
|
codex: 'AGENTS.md',
|
|
34
|
+
grok: 'AGENTS.md', // Grok Build reads the AGENTS.md family (and CLAUDE.md) natively
|
|
33
35
|
aider: 'AGENTS.md', // shares the Codex primer; aider is pointed at it via .aider.conf.yml
|
|
34
36
|
gemini: 'GEMINI.md',
|
|
35
37
|
copilot: '.github/copilot-instructions.md',
|
|
@@ -50,10 +52,13 @@ export const AIDER_CONF_FILE = '.aider.conf.yml';
|
|
|
50
52
|
* scratch (the `_vsd_tmp/`/`_convert_tmp/` dirs that bloated past deploys)
|
|
51
53
|
* can't leak in either. Reference material to KEEP (diagrams, decks, ADRs) goes
|
|
52
54
|
* in `docs/` instead - synced and versioned, but outside `src/` so it's never
|
|
53
|
-
* deployed.
|
|
55
|
+
* deployed. Build screenshots follow the same keep-but-don't-deploy pattern in
|
|
56
|
+
* `screenshots/` (page-screenshot.ts writes there; the server excludes both
|
|
57
|
+
* dirs from root deploys in s3-deploy.ts). Gitignore-glob form, matched by the
|
|
58
|
+
* `ignore` package in config.ts. */
|
|
54
59
|
export const SCRATCH_IGNORE = ['tmp/', '.tmp/', '*_tmp/', '.gipityscratch/'];
|
|
55
60
|
export const DEFAULT_SYNC_IGNORE = [
|
|
56
|
-
'node_modules', '.git', '.gipity.json', '.gipity/', '.claude/', '.gitignore', AIDER_CONF_FILE,
|
|
61
|
+
'node_modules', '.git', '.gipity.json', '.gipity/', '.claude/', '.codex/', '.gitignore', AIDER_CONF_FILE,
|
|
57
62
|
// Home-directory junk: a project created inside a real home dir (or one that
|
|
58
63
|
// shells out) sweeps in a cache dir + shell dotfiles that are never app
|
|
59
64
|
// files. `.cache/` alone can be gigabytes (it was 2.4 GB on one project),
|
|
@@ -145,7 +150,7 @@ export const LEGACY_MARKETPLACE_REPO = 'GipityAI/claude-plugin';
|
|
|
145
150
|
// an installed plugin when the marketplace advances - only an explicit
|
|
146
151
|
// `plugin install`/`update` does - so this constant is how a CLI upgrade tells
|
|
147
152
|
// ensureGipityPluginInstalled() to refresh a stale user-scope install.
|
|
148
|
-
export const GIPITY_PLUGIN_VERSION = '0.
|
|
153
|
+
export const GIPITY_PLUGIN_VERSION = '0.7.0';
|
|
149
154
|
/** True for hook commands the CLI itself wrote into settings.json in past
|
|
150
155
|
* versions. Matched by signature so migration strips exactly our own
|
|
151
156
|
* entries and never touches user-authored hooks. */
|
|
@@ -277,8 +282,8 @@ export function userScopeInstallState() {
|
|
|
277
282
|
export function userScopePluginCurrent() {
|
|
278
283
|
return userScopeInstallState().current;
|
|
279
284
|
}
|
|
280
|
-
function
|
|
281
|
-
const probe = spawnSyncCommand(process.platform === 'win32' ? 'where' : 'which', [
|
|
285
|
+
export function binaryOnPath(bin) {
|
|
286
|
+
const probe = spawnSyncCommand(process.platform === 'win32' ? 'where' : 'which', [bin], {
|
|
282
287
|
encoding: 'utf-8',
|
|
283
288
|
});
|
|
284
289
|
return probe.status === 0 && !!probe.stdout?.toString().trim();
|
|
@@ -301,7 +306,7 @@ export function ensureGipityPluginInstalled() {
|
|
|
301
306
|
const state = userScopeInstallState();
|
|
302
307
|
if (state.current)
|
|
303
308
|
return;
|
|
304
|
-
if (!
|
|
309
|
+
if (!binaryOnPath('claude'))
|
|
305
310
|
return;
|
|
306
311
|
// Refresh the marketplace clone so install/update resolves the current version.
|
|
307
312
|
// resolveCommand: on Windows `claude` is a .cmd shim that spawn can't launch
|
|
@@ -327,6 +332,209 @@ export function ensureGipityPluginInstalled() {
|
|
|
327
332
|
timeout: 120_000,
|
|
328
333
|
});
|
|
329
334
|
}
|
|
335
|
+
// --- Grok Build (xAI) ------------------------------------------------------
|
|
336
|
+
// Grok Build reads Claude-format plugins natively - skills/, commands/,
|
|
337
|
+
// hooks/hooks.json, and the .claude-plugin/plugin.json manifest - and sets
|
|
338
|
+
// CLAUDE_PLUGIN_ROOT/CLAUDE_PLUGIN_DATA aliases for plugin hooks, so the one
|
|
339
|
+
// GipityAI/skills repo serves both agents. A user-scope install
|
|
340
|
+
// (`grok plugin install <repo> --trust`) is trusted and enabled automatically,
|
|
341
|
+
// which gives Grok sessions the same skills and file-sync hooks as Claude Code.
|
|
342
|
+
/** Parsed install state for the Gipity plugin in Grok Build, read straight
|
|
343
|
+
* from ~/.grok/installed-plugins/registry.json (no subprocess). Mirrors
|
|
344
|
+
* {@link userScopeInstallState} for Claude Code. */
|
|
345
|
+
export function grokInstallState() {
|
|
346
|
+
try {
|
|
347
|
+
const p = join(homedir(), '.grok', 'installed-plugins', 'registry.json');
|
|
348
|
+
const data = JSON.parse(readFileSync(p, 'utf-8'));
|
|
349
|
+
const versions = [];
|
|
350
|
+
for (const repo of Object.values(data?.repos ?? {})) {
|
|
351
|
+
const v = repo?.plugins?.gipity?.version;
|
|
352
|
+
if (typeof v === 'string')
|
|
353
|
+
versions.push(v);
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
exists: versions.length > 0,
|
|
357
|
+
current: versions.some((v) => versionGte(v, GIPITY_PLUGIN_VERSION)),
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
return { exists: false, current: false };
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/** Install (or upgrade) the Gipity plugin in Grok Build. Best-effort and
|
|
365
|
+
* non-fatal, like the Claude Code counterpart: skips instantly when Grok
|
|
366
|
+
* isn't installed or the plugin is already current, so the steady-state cost
|
|
367
|
+
* is one registry.json read. */
|
|
368
|
+
export function ensureGrokPluginInstalled() {
|
|
369
|
+
const state = grokInstallState();
|
|
370
|
+
if (state.current)
|
|
371
|
+
return;
|
|
372
|
+
if (!binaryOnPath('grok'))
|
|
373
|
+
return;
|
|
374
|
+
const grokCmd = resolveCommand('grok');
|
|
375
|
+
// `install` clones the repo; on an existing install `update <name>` is the
|
|
376
|
+
// verb that refetches the source and advances the recorded version.
|
|
377
|
+
const verb = state.exists
|
|
378
|
+
? ['plugin', 'update', 'gipity']
|
|
379
|
+
: ['plugin', 'install', GIPITY_MARKETPLACE_REPO, '--trust'];
|
|
380
|
+
const res = spawnSyncCommand(grokCmd, verb, { stdio: 'ignore', timeout: 120_000 });
|
|
381
|
+
if (!state.exists && res.status === 0) {
|
|
382
|
+
console.log('Installed the Gipity plugin for Grok (skills + file-sync hooks).');
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
// --- OpenAI Codex ------------------------------------------------------------
|
|
386
|
+
// Codex has no Claude-plugin compatibility, but it reads the same SKILL.md
|
|
387
|
+
// skill format from the cross-agent `~/.agents/skills` directory (also read by
|
|
388
|
+
// OpenClaw and other agentskills.io adopters), and supports project hooks in
|
|
389
|
+
// `.codex/hooks.json` with Claude-style matcher groups (`Edit|Write` aliases
|
|
390
|
+
// its apply_patch tool). So Codex setup = copy the plugin's skills into
|
|
391
|
+
// ~/.agents/skills, stage its hook scripts under ~/.gipity/agent-hooks, and
|
|
392
|
+
// write a project .codex/hooks.json pointing at them. Codex requires the user
|
|
393
|
+
// to approve non-managed hooks once via /hooks - there is no supported way to
|
|
394
|
+
// pre-trust, so we print a nudge on first write.
|
|
395
|
+
export const AGENTS_SKILLS_DIR = join(homedir(), '.agents', 'skills');
|
|
396
|
+
export const AGENT_HOOKS_DIR = join(homedir(), '.gipity', 'agent-hooks');
|
|
397
|
+
/** Records what ensureAgentSkillsInstalled() put on this machine: the plugin
|
|
398
|
+
* version and the exact skill names copied, so upgrades replace and uninstall
|
|
399
|
+
* removes precisely those. */
|
|
400
|
+
export const AGENT_SKILLS_MANIFEST = join(homedir(), '.gipity', 'agent-skills.json');
|
|
401
|
+
export function agentSkillsState() {
|
|
402
|
+
try {
|
|
403
|
+
const m = JSON.parse(readFileSync(AGENT_SKILLS_MANIFEST, 'utf-8'));
|
|
404
|
+
return {
|
|
405
|
+
current: typeof m?.version === 'string' && versionGte(m.version, GIPITY_PLUGIN_VERSION),
|
|
406
|
+
skills: Array.isArray(m?.skills) ? m.skills : [],
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
return { current: false, skills: [] };
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
/** Materialize the Gipity skills into ~/.agents/skills (the cross-agent skills
|
|
414
|
+
* dir Codex reads) and the plugin's hook scripts into ~/.gipity/agent-hooks.
|
|
415
|
+
* Source of truth is the same GipityAI/skills repo the Claude/Grok plugin
|
|
416
|
+
* installs clone - fetched with a shallow git clone into a temp dir.
|
|
417
|
+
* Best-effort: no git, no network, or a failed clone all leave things as they
|
|
418
|
+
* were; the next init retries. */
|
|
419
|
+
export function ensureAgentSkillsInstalled() {
|
|
420
|
+
if (agentSkillsState().current)
|
|
421
|
+
return;
|
|
422
|
+
if (!binaryOnPath('git'))
|
|
423
|
+
return;
|
|
424
|
+
const tmp = mkdtempSync(join(tmpdir(), 'gipity-skills-'));
|
|
425
|
+
try {
|
|
426
|
+
const clone = spawnSyncCommand(resolveCommand('git'), ['clone', '--depth', '1', `https://github.com/${GIPITY_MARKETPLACE_REPO}.git`, join(tmp, 'repo')], { stdio: 'ignore', timeout: 120_000 });
|
|
427
|
+
if (clone.status !== 0)
|
|
428
|
+
return;
|
|
429
|
+
const repo = join(tmp, 'repo');
|
|
430
|
+
let version = GIPITY_PLUGIN_VERSION;
|
|
431
|
+
try {
|
|
432
|
+
const manifest = JSON.parse(readFileSync(join(repo, '.claude-plugin', 'plugin.json'), 'utf-8'));
|
|
433
|
+
if (typeof manifest?.version === 'string')
|
|
434
|
+
version = manifest.version;
|
|
435
|
+
}
|
|
436
|
+
catch { /* keep the CLI's pinned version */ }
|
|
437
|
+
const skillsSrc = join(repo, 'skills');
|
|
438
|
+
const names = [];
|
|
439
|
+
for (const entry of readdirSync(skillsSrc, { withFileTypes: true })) {
|
|
440
|
+
if (!entry.isDirectory())
|
|
441
|
+
continue;
|
|
442
|
+
if (!existsSync(join(skillsSrc, entry.name, 'SKILL.md')))
|
|
443
|
+
continue;
|
|
444
|
+
mkdirSync(AGENTS_SKILLS_DIR, { recursive: true });
|
|
445
|
+
cpSync(join(skillsSrc, entry.name), join(AGENTS_SKILLS_DIR, entry.name), {
|
|
446
|
+
recursive: true,
|
|
447
|
+
force: true,
|
|
448
|
+
});
|
|
449
|
+
names.push(entry.name);
|
|
450
|
+
}
|
|
451
|
+
mkdirSync(AGENT_HOOKS_DIR, { recursive: true });
|
|
452
|
+
for (const script of readdirSync(join(repo, 'hooks', 'scripts'))) {
|
|
453
|
+
cpSync(join(repo, 'hooks', 'scripts', script), join(AGENT_HOOKS_DIR, script), { force: true });
|
|
454
|
+
}
|
|
455
|
+
writeFileSync(AGENT_SKILLS_MANIFEST, JSON.stringify({ version, skills: names }, null, 2) + '\n');
|
|
456
|
+
console.log(`Installed ${names.length} Gipity skills for Codex (~/.agents/skills).`);
|
|
457
|
+
}
|
|
458
|
+
catch { /* best-effort - never break setup */ }
|
|
459
|
+
finally {
|
|
460
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/** Pure core of the .codex/hooks.json merge: given the file's current content
|
|
464
|
+
* (`null` when absent), return the new content, or `null` when no change is
|
|
465
|
+
* needed. Adds the Gipity sync hook groups (push on file edits, pull before
|
|
466
|
+
* each prompt) AND the session-capture groups (SessionStart / throttled
|
|
467
|
+
* PostToolUse / Stop → capture.cjs, which mirrors the session into the web
|
|
468
|
+
* CLI; Codex has no SessionEnd or SubagentStop, so those are absent), while
|
|
469
|
+
* preserving any user-authored hooks. Our groups are recognized by their
|
|
470
|
+
* exact command string, so a re-run upgrades older sync-only files by adding
|
|
471
|
+
* just the missing capture entries. Exported for unit testing. */
|
|
472
|
+
export function applyCodexHooks(existing) {
|
|
473
|
+
const launcher = join(AGENT_HOOKS_DIR, 'launch.sh');
|
|
474
|
+
const cmd = (script, ...args) => [`sh "${launcher}" "${join(AGENT_HOOKS_DIR, script)}"`, ...args].join(' ');
|
|
475
|
+
const wanted = [
|
|
476
|
+
{ event: 'PostToolUse', matcher: 'Edit|Write', command: cmd('sync-push.cjs'), timeout: 30 },
|
|
477
|
+
{ event: 'UserPromptSubmit', command: cmd('sync-pull.cjs'), timeout: 300 },
|
|
478
|
+
// Session capture: mirror the Codex session into the Gipity web CLI.
|
|
479
|
+
{ event: 'SessionStart', command: cmd('capture.cjs', 'codex', 'session-start'), timeout: 30 },
|
|
480
|
+
{ event: 'PostToolUse', command: cmd('capture.cjs', 'codex', 'post-tool-use'), timeout: 30 },
|
|
481
|
+
{ event: 'Stop', command: cmd('capture.cjs', 'codex', 'stop'), timeout: 60 },
|
|
482
|
+
];
|
|
483
|
+
let settings = {};
|
|
484
|
+
if (existing !== null) {
|
|
485
|
+
try {
|
|
486
|
+
settings = JSON.parse(existing);
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
return null; // user file we can't parse - leave it alone
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
const hooks = settings.hooks ?? (settings.hooks = {});
|
|
493
|
+
let changed = false;
|
|
494
|
+
for (const w of wanted) {
|
|
495
|
+
const groups = Array.isArray(hooks[w.event]) ? hooks[w.event] : (hooks[w.event] = []);
|
|
496
|
+
const present = groups.some((g) => Array.isArray(g?.hooks) && g.hooks.some((h) => typeof h?.command === 'string' && h.command === w.command));
|
|
497
|
+
if (present)
|
|
498
|
+
continue;
|
|
499
|
+
const group = { hooks: [{ type: 'command', command: w.command, timeout: w.timeout }] };
|
|
500
|
+
if (w.matcher)
|
|
501
|
+
group.matcher = w.matcher;
|
|
502
|
+
groups.push(group);
|
|
503
|
+
changed = true;
|
|
504
|
+
}
|
|
505
|
+
return changed ? JSON.stringify(settings, null, 2) + '\n' : null;
|
|
506
|
+
}
|
|
507
|
+
/** Write the project-level Codex hooks (.codex/hooks.json). POSIX only - the
|
|
508
|
+
* commands run through the same sh launcher the plugin uses; Codex on Windows
|
|
509
|
+
* would need commandWindows variants (not wired yet). */
|
|
510
|
+
export function setupCodexHooks() {
|
|
511
|
+
if (process.platform === 'win32')
|
|
512
|
+
return;
|
|
513
|
+
const cwd = resolve(process.cwd());
|
|
514
|
+
if (cwd === resolve(homedir()))
|
|
515
|
+
return; // never treat $HOME as a project
|
|
516
|
+
const path = join(cwd, '.codex', 'hooks.json');
|
|
517
|
+
const existing = existsSync(path) ? readFileSync(path, 'utf-8') : null;
|
|
518
|
+
const next = applyCodexHooks(existing);
|
|
519
|
+
if (next === null)
|
|
520
|
+
return;
|
|
521
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
522
|
+
writeFileSync(path, next);
|
|
523
|
+
if (existing === null) {
|
|
524
|
+
console.log('Wrote Codex sync + session-capture hooks (.codex/hooks.json) - approve them once with /hooks inside Codex.');
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
console.log('Updated Codex hooks (.codex/hooks.json) - if Codex asks, re-approve them via /hooks.');
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
/** Full Codex integration: user-scope skills + project sync hooks. Gated on
|
|
531
|
+
* the codex binary so machines without Codex get only the AGENTS.md primer. */
|
|
532
|
+
export function setupCodexIntegration() {
|
|
533
|
+
if (!binaryOnPath('codex'))
|
|
534
|
+
return;
|
|
535
|
+
ensureAgentSkillsInstalled();
|
|
536
|
+
setupCodexHooks();
|
|
537
|
+
}
|
|
330
538
|
export function setupClaudeHooks() {
|
|
331
539
|
// All hooks ship in the plugin - enable it at user scope (and clean up any
|
|
332
540
|
// legacy hook blocks in the user-global settings while we're there).
|
|
@@ -371,9 +579,25 @@ export const GIPITY_BLOCK_END = '<!-- END GIPITY INTEGRATION -->';
|
|
|
371
579
|
* them into a generated doc is the wrong layer. The CLI surfaces them where the
|
|
372
580
|
* agent actually looks: `gipity deploy` prints the live URL, `gipity status` and
|
|
373
581
|
* `gipity project info` show the URL + GUID. That keeps them authoritative and
|
|
374
|
-
* avoids stale values frozen into a file.
|
|
375
|
-
|
|
376
|
-
|
|
582
|
+
* avoids stale values frozen into a file.
|
|
583
|
+
*
|
|
584
|
+
* The one environment value that DOES live here is the API base. The baked
|
|
585
|
+
* knowledge text names `https://a.gipity.ai` as the app-services endpoint;
|
|
586
|
+
* when this session runs against a different platform instance (GIPITY_API_BASE
|
|
587
|
+
* / --api-base, e.g. a local dev server), the project only exists there - an
|
|
588
|
+
* agent that copies the public host into app code gets 404s it can't explain.
|
|
589
|
+
* So the block is rendered against the resolved base, with a note naming the
|
|
590
|
+
* instance. The block is fully regenerated each session, so it tracks the
|
|
591
|
+
* environment rather than going stale. */
|
|
592
|
+
function renderManagedBlock(apiBase) {
|
|
593
|
+
let body = [SKILLS_CONTENT, BUILD_VS_NON_BUILD_RULE, DEFINITION_OF_DONE].join('\n\n');
|
|
594
|
+
const base = apiBase.replace(/\/+$/, '');
|
|
595
|
+
if (base !== DEFAULT_API_BASE) {
|
|
596
|
+
body = body.replaceAll(DEFAULT_API_BASE, base);
|
|
597
|
+
const note = `> **Platform instance:** this project runs against the Gipity platform at \`${base}\`, not the public \`${DEFAULT_API_BASE}\`. The project and its data exist only on that instance; every API/service URL in this document already points there - never substitute \`a.gipity.ai\`.`;
|
|
598
|
+
const headingEnd = body.indexOf('\n');
|
|
599
|
+
body = body.slice(0, headingEnd + 1) + '\n' + note + '\n' + body.slice(headingEnd + 1);
|
|
600
|
+
}
|
|
377
601
|
return `${GIPITY_BLOCK_BEGIN}\n${body}\n${GIPITY_BLOCK_END}`;
|
|
378
602
|
}
|
|
379
603
|
/**
|
|
@@ -393,8 +617,8 @@ function renderManagedBlock() {
|
|
|
393
617
|
*
|
|
394
618
|
* Exported for unit testing.
|
|
395
619
|
*/
|
|
396
|
-
export function applySkillsBlock(existing) {
|
|
397
|
-
const block = renderManagedBlock();
|
|
620
|
+
export function applySkillsBlock(existing, apiBase = DEFAULT_API_BASE) {
|
|
621
|
+
const block = renderManagedBlock(apiBase);
|
|
398
622
|
if (existing === null)
|
|
399
623
|
return block + '\n';
|
|
400
624
|
let next;
|
|
@@ -422,7 +646,7 @@ function writeSkillsFile(relPath, wrap) {
|
|
|
422
646
|
const path = resolve(process.cwd(), relPath);
|
|
423
647
|
mkdirSync(dirname(path), { recursive: true });
|
|
424
648
|
const existing = existsSync(path) ? readFileSync(path, 'utf-8') : null;
|
|
425
|
-
const baseNext = applySkillsBlock(existing);
|
|
649
|
+
const baseNext = applySkillsBlock(existing, resolveApiBase());
|
|
426
650
|
// Frontmatter wrap only applies on first write (no existing content).
|
|
427
651
|
// Re-runs preserve user content outside the managed block, including
|
|
428
652
|
// any frontmatter they may have added themselves.
|
|
@@ -506,15 +730,22 @@ export function setupCopilotMd() {
|
|
|
506
730
|
export function setupCursorMd() {
|
|
507
731
|
writeSkillsFile(PRIMER_FILES.cursor, (block) => `---\ndescription: Gipity platform integration - CLI, sandbox, app services\nalwaysApply: true\n---\n\n${block}`);
|
|
508
732
|
}
|
|
509
|
-
/** All supported coding
|
|
510
|
-
*
|
|
733
|
+
/** All supported coding tools: the primer each gets (`setup`) plus, for tools
|
|
734
|
+
* with a deeper Gipity integration, an `integrate` step that installs the
|
|
735
|
+
* Gipity skills and file-sync hooks into that tool's own ecosystem (Claude
|
|
736
|
+
* Code plugin, Grok Build plugin, Codex ~/.agents/skills + .codex hooks).
|
|
737
|
+
* Integrations are best-effort and self-gating - each no-ops fast when its
|
|
738
|
+
* binary is missing or the install is already current - so running the whole
|
|
739
|
+
* default set on every init/launch is cheap and keeps all detected agents in
|
|
740
|
+
* lockstep. Order matters for help-text rendering and the `all` expansion.
|
|
511
741
|
* `optIn` tools are excluded from the default / `all` set and must be named
|
|
512
742
|
* explicitly (`--for aider`): aider's setup writes `.aider.conf.yml`, which
|
|
513
743
|
* changes how aider behaves in this directory - a heavier footprint than
|
|
514
744
|
* dropping an inert markdown primer. */
|
|
515
745
|
export const SUPPORTED_TOOLS = [
|
|
516
|
-
{ key: 'claude', label: 'Claude Code (CLAUDE.md)', setup: setupClaudeMd },
|
|
517
|
-
{ key: 'codex', label: 'OpenAI Codex (AGENTS.md)', setup: setupAgentsMd },
|
|
746
|
+
{ key: 'claude', label: 'Claude Code (CLAUDE.md + Gipity plugin)', setup: setupClaudeMd, integrate: setupClaudeHooks },
|
|
747
|
+
{ key: 'codex', label: 'OpenAI Codex (AGENTS.md + skills + sync hooks)', setup: setupAgentsMd, integrate: setupCodexIntegration },
|
|
748
|
+
{ key: 'grok', label: 'Grok Build (AGENTS.md + Gipity plugin)', setup: setupAgentsMd, integrate: ensureGrokPluginInstalled },
|
|
518
749
|
{ key: 'aider', label: 'Aider (AGENTS.md + .aider.conf.yml)', setup: setupAiderMd, optIn: true },
|
|
519
750
|
{ key: 'gemini', label: 'Gemini CLI (GEMINI.md)', setup: setupGeminiMd },
|
|
520
751
|
{ key: 'copilot', label: 'GitHub Copilot (.github/copilot-instructions.md)', setup: setupCopilotMd },
|
|
@@ -523,6 +754,19 @@ export const SUPPORTED_TOOLS = [
|
|
|
523
754
|
/** The primer set written when the user makes no explicit `--for` choice:
|
|
524
755
|
* every tool except opt-in ones. */
|
|
525
756
|
export const DEFAULT_TOOLS = SUPPORTED_TOOLS.filter(t => !t.optIn);
|
|
757
|
+
/** Registry-driven project setup - the single entry point every "link this
|
|
758
|
+
* directory" path shares (`init`, `project create`, `gipity claude`, the
|
|
759
|
+
* relay daemon). Writes each requested tool's primer, runs its integration
|
|
760
|
+
* (hooks/skills install) when it has one, and refreshes .gitignore. Replaces
|
|
761
|
+
* the setupClaudeHooks/setupClaudeMd/setupAgentsMd/setupGitignore quartet
|
|
762
|
+
* that used to be copy-pasted per call site and silently skipped newer tools. */
|
|
763
|
+
export function setupProjectTools(tools = DEFAULT_TOOLS) {
|
|
764
|
+
for (const t of tools) {
|
|
765
|
+
t.setup();
|
|
766
|
+
t.integrate?.();
|
|
767
|
+
}
|
|
768
|
+
setupGitignore();
|
|
769
|
+
}
|
|
526
770
|
export function setupGitignore() {
|
|
527
771
|
const gitignorePath = resolve(process.cwd(), '.gitignore');
|
|
528
772
|
// Sync already skips the scratch namespaces (DEFAULT_SYNC_IGNORE); ignore them
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gipity",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "The full-stack platform tuned for AI agents. Database, storage, auth, functions, deploy, and drop-in kits - all agent-tuned. Pair with Claude Code or use standalone.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"gipity": "dist/updater/shim.js",
|
|
@@ -14,13 +14,14 @@
|
|
|
14
14
|
"prepack": "npm run build",
|
|
15
15
|
"dev": "tsc --watch",
|
|
16
16
|
"test": "npm run test:smoke",
|
|
17
|
-
"test:smoke": "
|
|
17
|
+
"test:smoke": "npm run build && node --test dist/__tests__/utils.test.js dist/__tests__/platform.test.js dist/__tests__/colors.test.js dist/__tests__/config.test.js dist/__tests__/sync.test.js dist/__tests__/sync-apply.test.js dist/__tests__/sync-unretrievable.test.js dist/__tests__/sync-clean-check.test.js dist/__tests__/sync-lock.test.js dist/__tests__/auth-lock.test.js dist/__tests__/api-401-retry.test.js dist/__tests__/push-cas.test.js dist/__tests__/upload.test.js dist/__tests__/progress.test.js dist/__tests__/updater.test.js dist/__tests__/cli-smoke.test.js dist/__tests__/claude-noninteractive.test.js dist/__tests__/claude-trust.test.js dist/__tests__/relay-state.test.js dist/__tests__/relay-daemon.test.js dist/__tests__/session-pool.test.js dist/__tests__/relay-diagnostics.test.js dist/__tests__/relay-installers.test.js dist/__tests__/relay-bridge-abort.test.js dist/__tests__/relay-redact.test.js dist/__tests__/relay-machine-id.test.js dist/__tests__/stream-json.test.js dist/__tests__/ingest-queue.test.js dist/__tests__/media-upload.test.js dist/__tests__/stream-delta.test.js dist/__tests__/phase-tracker.test.js dist/__tests__/relay-ingest-contract.test.js dist/__tests__/prompts.test.js dist/__tests__/capture-transcript.test.js dist/__tests__/capture-parsers.test.js dist/__tests__/agents.test.js dist/__tests__/capture-lock.test.js dist/__tests__/capture-resolve.test.js dist/__tests__/flag-aliases.test.js dist/__tests__/client-context.test.js dist/__tests__/adopt-cwd.test.js dist/__tests__/cli-cmd-agent.test.js dist/__tests__/cli-cmd-approval.test.js dist/__tests__/cli-cmd-audit.test.js dist/__tests__/cli-cmd-bug.test.js dist/__tests__/cli-cmd-bug-queue.test.js dist/__tests__/cli-cmd-chat.test.js dist/__tests__/cli-cmd-credits.test.js dist/__tests__/cli-cmd-db.test.js dist/__tests__/cli-cmd-deploy.test.js dist/__tests__/cli-cmd-domain.test.js dist/__tests__/cli-cmd-email.test.js dist/__tests__/cli-cmd-file.test.js dist/__tests__/cli-cmd-storage.test.js dist/__tests__/cli-cmd-fn.test.js dist/__tests__/cli-cmd-service.test.js dist/__tests__/cli-cmd-job.test.js dist/__tests__/cli-cmd-generate.test.js dist/__tests__/cli-cmd-gmail.test.js dist/__tests__/cli-cmd-info.test.js dist/__tests__/cli-cmd-init.test.js dist/__tests__/cli-cmd-location.test.js dist/__tests__/cli-cmd-text.test.js dist/__tests__/cli-cmd-login.test.js dist/__tests__/cli-cmd-logout.test.js dist/__tests__/cli-cmd-token.test.js dist/__tests__/cli-cmd-logs.test.js dist/__tests__/cli-cmd-memory.test.js dist/__tests__/cli-cmd-page.test.js dist/__tests__/cli-cmd-plan.test.js dist/__tests__/cli-cmd-project.test.js dist/__tests__/cli-cmd-rbac.test.js dist/__tests__/cli-cmd-realtime.test.js dist/__tests__/cli-cmd-records.test.js dist/__tests__/cli-cmd-relay.test.js dist/__tests__/cli-cmd-setup.test.js dist/__tests__/cli-cmd-doctor.test.js dist/__tests__/claude-setup.test.js dist/__tests__/cli-cmd-sandbox.test.js dist/__tests__/cli-cmd-add.test.js dist/__tests__/cli-cmd-remove.test.js dist/__tests__/cli-cmd-skill.test.js dist/__tests__/cli-cmd-test.test.js dist/__tests__/cli-cmd-workflow.test.js dist/__tests__/setup-skills-block.test.js dist/__tests__/setup-hooks.test.js dist/__tests__/setup-codex-hooks.test.js dist/__tests__/trace.test.js",
|
|
18
18
|
"test:smoke:quick": "node scripts/smoke-quick.mjs",
|
|
19
19
|
"test:e2e": "tsc && GIPITY_E2E=1 node --test --test-timeout=180000 dist/__tests__/cli-e2e-live.test.js dist/__tests__/cli-e2e-sync-live.test.js dist/__tests__/cli-e2e-sync-stress-live.test.js dist/__tests__/cli-e2e-rollback-live.test.js dist/__tests__/cli-e2e-services-media-live.test.js dist/__tests__/cli-e2e-workflow-live.test.js dist/__tests__/cli-e2e-sandbox-live.test.js dist/__tests__/cli-e2e-page-fetch-live.test.js dist/__tests__/cli-e2e-page-test-live.test.js",
|
|
20
20
|
"test:e2e:sync": "tsc && GIPITY_E2E=1 node --test --test-timeout=180000 dist/__tests__/cli-e2e-sync-live.test.js",
|
|
21
21
|
"test:e2e:sync-stress": "tsc && GIPITY_E2E=1 node --test --test-timeout=180000 dist/__tests__/cli-e2e-sync-stress-live.test.js",
|
|
22
22
|
"test:e2e:rollback": "tsc && GIPITY_E2E=1 node --test --test-timeout=180000 dist/__tests__/cli-e2e-rollback-live.test.js",
|
|
23
|
-
"test:e2e:sandbox": "tsc && GIPITY_E2E=1 node --test --test-timeout=180000 dist/__tests__/cli-e2e-sandbox-live.test.js"
|
|
23
|
+
"test:e2e:sandbox": "tsc && GIPITY_E2E=1 node --test --test-timeout=180000 dist/__tests__/cli-e2e-sandbox-live.test.js",
|
|
24
|
+
"test:e2e:agents": "tsc && GIPITY_E2E=1 node --test --test-timeout=900000 dist/__tests__/cli-e2e-agents-live.test.js"
|
|
24
25
|
},
|
|
25
26
|
"dependencies": {
|
|
26
27
|
"@anthropic-ai/claude-agent-sdk": "^0.3.207",
|