gipity 1.0.428 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/agents/claude-code.js +53 -0
  2. package/dist/agents/codex.js +49 -0
  3. package/dist/agents/grok.js +54 -0
  4. package/dist/agents/index.js +23 -0
  5. package/dist/agents/types.js +18 -0
  6. package/dist/api.js +7 -0
  7. package/dist/capture/sources/codex.js +216 -0
  8. package/dist/capture/sources/grok.js +178 -0
  9. package/dist/client-context.js +2 -0
  10. package/dist/commands/build.js +1352 -0
  11. package/dist/commands/chat.js +9 -3
  12. package/dist/commands/claude.js +4 -13
  13. package/dist/commands/db.js +12 -5
  14. package/dist/commands/deploy.js +19 -1
  15. package/dist/commands/doctor.js +8 -2
  16. package/dist/commands/generate.js +61 -4
  17. package/dist/commands/init.js +9 -15
  18. package/dist/commands/page-eval.js +404 -56
  19. package/dist/commands/page-inspect.js +15 -5
  20. package/dist/commands/page-screenshot.js +269 -64
  21. package/dist/commands/project.js +1 -1
  22. package/dist/commands/relay-install.js +1 -1
  23. package/dist/commands/relay.js +2 -2
  24. package/dist/commands/sandbox.js +62 -16
  25. package/dist/commands/setup.js +16 -10
  26. package/dist/commands/uninstall.js +25 -3
  27. package/dist/hooks/capture-runner.js +136 -39
  28. package/dist/index.js +1962 -668
  29. package/dist/knowledge.js +3 -3
  30. package/dist/page-fixtures.js +92 -3
  31. package/dist/prefs.js +50 -0
  32. package/dist/project-setup.js +2 -10
  33. package/dist/provider-docs.js +10 -10
  34. package/dist/relay/daemon.js +140 -18
  35. package/dist/relay/device-http.js +22 -0
  36. package/dist/relay/diagnostics.js +4 -2
  37. package/dist/relay/media-upload.js +184 -0
  38. package/dist/relay/onboarding.js +2 -2
  39. package/dist/setup.js +262 -18
  40. package/package.json +4 -3
@@ -0,0 +1,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
@@ -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 claude` runs)?', { default: 'yes' });
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 claude` restarts it. For boot-time auto-start:')}`);
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. Gitignore-glob form, matched by the `ignore` package in config.ts. */
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.5.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 claudeOnPath() {
281
- const probe = spawnSyncCommand(process.platform === 'win32' ? 'where' : 'which', ['claude'], {
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 (!claudeOnPath())
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
- function renderManagedBlock() {
376
- const body = [SKILLS_CONTENT, BUILD_VS_NON_BUILD_RULE, DEFINITION_OF_DONE].join('\n\n');
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-tool primers and the function that writes each.
510
- * Order matters for help-text rendering and the `all` expansion.
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.0.428",
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": "tsc && 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__/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-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__/trace.test.js",
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",