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
@@ -50,9 +50,15 @@ export const chatCommand = new Command('chat')
50
50
  ...(a.remoteSize != null ? { size: a.remoteSize } : {}),
51
51
  }));
52
52
  }
53
+ // The server's cost-consent gate stops the agentic loop on big-build
54
+ // requests: content comes back empty with an explicit costWarning.
55
+ // Surface it - before this the CLI printed a blank line and the request
56
+ // silently did nothing. Re-running the command consents via chat reply.
57
+ const displayContent = res.data.content || res.data.costWarning?.message || '';
53
58
  if (opts.json) {
54
59
  console.log(JSON.stringify({
55
- content: res.data.content,
60
+ content: displayContent,
61
+ costWarning: res.data.costWarning ?? null,
56
62
  toolsUsed: res.data.toolsUsed?.map(t => ({
57
63
  tool: t.toolName,
58
64
  success: t.success,
@@ -67,8 +73,8 @@ export const chatCommand = new Command('chat')
67
73
  }));
68
74
  }
69
75
  else {
70
- // Show agent response
71
- console.log(res.data.content);
76
+ // Show agent response (or the cost warning when the gate stopped the loop)
77
+ console.log(displayContent);
72
78
  // Show tools used
73
79
  if (res.data.toolsUsed && res.data.toolsUsed.length > 0) {
74
80
  const toolNames = [...new Set(res.data.toolsUsed.map(t => t.toolName))];
@@ -9,7 +9,7 @@ import { get, post, ApiError, getAccountSlug } from '../api.js';
9
9
  import { interactiveLogin } from '../login-flow.js';
10
10
  import { getConfig, saveConfigAt, clearConfigCache, getApiBaseOverride, DEFAULT_API_BASE, getConfigPath } from '../config.js';
11
11
  import { sync } from '../sync.js';
12
- import { slugify, setupClaudeHooks, ensureGipityPluginInstalled, setupClaudeMd, setupAgentsMd, setupGitignore, DEFAULT_SYNC_IGNORE, isSyncIgnored } from '../setup.js';
12
+ import { slugify, ensureGipityPluginInstalled, setupProjectTools, DEFAULT_SYNC_IGNORE, isSyncIgnored } from '../setup.js';
13
13
  import { buildProjectContextBlock as buildProjectContextBlockText, buildNewProjectPrompt, buildResumeWrap, buildFreshWrap, } from '../prompts.js';
14
14
  import * as relayState from '../relay/state.js';
15
15
  import { maybeOfferRelayOn, ensureDaemonRunning } from '../relay/onboarding.js';
@@ -481,10 +481,7 @@ export const claudeCommand = new Command('claude')
481
481
  catch (err) {
482
482
  console.log(` ${warning('Could not sync files (will retry on next prompt):')} ${err.message}`);
483
483
  }
484
- setupClaudeHooks();
485
- setupClaudeMd();
486
- setupAgentsMd();
487
- setupGitignore();
484
+ setupProjectTools();
488
485
  if (nonInteractive) {
489
486
  // Headless: the -p message is wrapped later (Step 3). Just record
490
487
  // whether to use the new-project framing for that wrap.
@@ -503,10 +500,7 @@ export const claudeCommand = new Command('claude')
503
500
  // cwd already has (or inherits) a .gipity.json - run inside it.
504
501
  console.log(` Project: ${brand(existing.projectSlug)} ${muted(`(${existing.projectGuid})`)}`);
505
502
  console.log(` ${success('Already set up.')}\n`);
506
- setupClaudeHooks();
507
- setupClaudeMd();
508
- setupAgentsMd();
509
- setupGitignore();
503
+ setupProjectTools();
510
504
  // Warn before syncing a very large project tree. An oversized root
511
505
  // (or a config accidentally rooted high up, e.g. at $HOME) makes the
512
506
  // sync slow, and a silent multi-GB walk reads as a hang.
@@ -658,10 +652,7 @@ export const claudeCommand = new Command('claude')
658
652
  // of done, and a welcome banner prints before launch - the user tells
659
653
  // Claude directly what they want to build or do.
660
654
  initialPrompt = '';
661
- setupClaudeHooks();
662
- setupClaudeMd();
663
- setupAgentsMd();
664
- setupGitignore();
655
+ setupProjectTools();
665
656
  console.log(` ${success(`Project "${project.name}" ready.`)}\n`);
666
657
  }
667
658
  // ── Step 3: Launch Claude Code ────────────────────────────────────
@@ -1,5 +1,5 @@
1
1
  import { Command } from 'commander';
2
- import { get, post, sendMessage } from '../api.js';
2
+ import { get, post } from '../api.js';
3
3
  import { requireConfig } from '../config.js';
4
4
  import { error as clrError, success } from '../colors.js';
5
5
  import { run, printList, emitField } from '../helpers/index.js';
@@ -106,13 +106,20 @@ dbCommand
106
106
  .description('Create a database')
107
107
  .option('--json', 'Output as JSON')
108
108
  .action((name, opts) => run('Create', async () => {
109
- // DDL delegates to agent (needs confirmation flow)
110
- const response = await sendMessage(`Create a new database called "${name}" for this project. Confirm when done, no explanation.`);
109
+ // Direct REST create - same endpoint `db drop` uses. This used to delegate
110
+ // to agent chat, which burned LLM tokens and, after the cost-consent gate
111
+ // landed server-side, could return an empty response having created
112
+ // nothing. Creating a database isn't destructive, so no confirmation.
113
+ const config = requireConfig();
114
+ await post(`/projects/${config.projectGuid}/db/manage`, {
115
+ action: 'create',
116
+ name,
117
+ });
111
118
  if (opts.json) {
112
- console.log(JSON.stringify({ response }));
119
+ console.log(JSON.stringify({ success: true, name }));
113
120
  }
114
121
  else {
115
- console.log(response);
122
+ console.log(success(`Created database '${name}'.`));
116
123
  }
117
124
  }));
118
125
  dbCommand
@@ -28,11 +28,24 @@ export const deployCommand = new Command('deploy')
28
28
  .option('--no-optimize', 'Skip build optimization and upload files as-is - the escape hatch for plain-HTML apps whose <script src> tags are not type="module"')
29
29
  .option('--json', 'Output as JSON')
30
30
  .option('--inspect [path]', 'After a successful deploy, run `page inspect` on the deployed URL (or URL + path) in the same command - one build-loop step instead of two. With --json, emits two JSON lines: deploy, then inspect.')
31
+ // The settle knobs `page inspect` already has. An app whose first paint is behind
32
+ // a model load / async boot inspects as an empty page without them, so a caller
33
+ // that reaches for `--inspect` on such an app needs them HERE - it should not have
34
+ // to abandon the combined command and re-run inspect standalone just to wait.
35
+ .option('--wait <ms>', 'With --inspect: sleep this many ms before capturing (max 30000)')
36
+ .option('--wait-for <selector>', "With --inspect: wait until this CSS selector appears before capturing (deterministic; the app's own ready signal)")
37
+ .option('--wait-timeout <ms>', 'With --inspect: max ms to wait for --wait-for before giving up', '5000')
31
38
  .action((target, opts) => run('Deploy', async () => {
32
39
  if (target !== 'dev' && target !== 'prod') {
33
40
  console.error(clrError('Target must be "dev" or "prod"'));
34
41
  process.exit(1);
35
42
  }
43
+ // A settle flag only means one thing on a deploy: "verify the page once it's
44
+ // up, and give it time to come up." Asking for it without --inspect is not an
45
+ // error to bounce back — it's the intent, so honour it.
46
+ if (!opts.inspect && (opts.waitFor || deployCommand.getOptionValueSource('wait') !== undefined)) {
47
+ opts.inspect = true;
48
+ }
36
49
  const config = requireConfig();
37
50
  await syncBeforeAction(opts);
38
51
  const doDeploy = () => post(`/projects/${config.projectGuid}/deploy`, {
@@ -61,7 +74,12 @@ export const deployCommand = new Command('deploy')
61
74
  if (!opts.json)
62
75
  console.log('');
63
76
  try {
64
- await inspectPage(url, { json: opts.json });
77
+ await inspectPage(url, {
78
+ json: opts.json,
79
+ wait: opts.wait,
80
+ waitFor: opts.waitFor,
81
+ waitTimeout: opts.waitTimeout,
82
+ });
65
83
  }
66
84
  catch (err) {
67
85
  console.error(warning(`Inspect failed (deploy itself succeeded): ${err?.message ?? err}`));
@@ -1,6 +1,6 @@
1
1
  import { Command } from 'commander';
2
2
  import { existsSync, readFileSync, statSync } from 'fs';
3
- import { join } from 'path';
3
+ import { join, resolve } from 'path';
4
4
  import { homedir } from 'os';
5
5
  import { LOCAL_PKG_DIR, LOCAL_ENTRY, STATE_FILE, SETTINGS_FILE, UPDATE_LOG, readState, readSettings, updatesDisabled } from '../updater/state.js';
6
6
  import { bold, dim, success, warning, error as clrError, muted } from '../colors.js';
@@ -162,7 +162,13 @@ export const doctorCommand = new Command('doctor')
162
162
  console.log(`${muted('claude code ')} installed ${yn(env.claude.installed)} · authenticated ${yn(env.claude.authenticated)}`);
163
163
  const autostartLabel = env.relay.autostart === null ? muted('n/a') : yn(env.relay.autostart);
164
164
  console.log(`${muted('relay ')} paired ${yn(env.relay.paired)} · running ${yn(env.relay.running)} · autostart ${autostartLabel}${env.relay.paused ? warning(' · paused') : ''}${env.relay.device ? muted(` (${env.relay.device.name})`) : ''}`);
165
- console.log(`${muted('ready ')} ${env.ready ? success('yes') : warning('no - run `gipity claude` (or the desktop app) to finish setup')}`);
165
+ // Codex approves project hooks interactively and stores the decision in
166
+ // its own opaque state - we can't verify it from here, so surface the
167
+ // one manual step whenever this project ships Codex hooks.
168
+ if (existsSync(resolve(process.cwd(), '.codex', 'hooks.json'))) {
169
+ console.log(`${muted('codex hooks ')} ${warning('written')} - session capture needs a one-time approval: run /hooks inside Codex in this project`);
170
+ }
171
+ console.log(`${muted('ready ')} ${env.ready ? success('yes') : warning('no - run `gipity build` (or the desktop app) to finish setup')}`);
166
172
  // ── CLI install / update health ────────────────────────────────────
167
173
  const state = readState();
168
174
  const settings = readSettings();
@@ -25,12 +25,66 @@ async function downloadFile(url, filename) {
25
25
  if (!res.ok)
26
26
  throw new Error(`Download failed: ${res.status}`);
27
27
  const buffer = Buffer.from(await res.arrayBuffer());
28
- ensureOutputDir(filename);
29
- writeFileSync(filename, buffer);
30
- const savedPath = resolvePath(filename);
28
+ const outPath = correctExtension(filename, buffer);
29
+ ensureOutputDir(outPath);
30
+ writeFileSync(outPath, buffer);
31
+ const savedPath = resolvePath(outPath);
31
32
  await pushGenerated(savedPath);
32
33
  return savedPath;
33
34
  }
35
+ /** What these bytes ACTUALLY are, by magic number — the only trustworthy source.
36
+ * A provider hands back whatever its model produced (BFL's "png" request comes
37
+ * back as JPEG), so neither the caller's -o extension nor the response's
38
+ * content_type describes the file on disk. */
39
+ function sniffExt(buf) {
40
+ // subarray past the end yields a short string, so every compare below is
41
+ // length-safe on its own — no blanket minimum (which would skip the check
42
+ // entirely on a small file and hand back the misnamed path).
43
+ const ascii = (start, end) => buf.subarray(start, end).toString('latin1');
44
+ if (buf.length < 4)
45
+ return undefined;
46
+ if (buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff)
47
+ return 'jpg';
48
+ if (ascii(0, 8) === '\x89PNG\r\n\x1a\n')
49
+ return 'png';
50
+ if (ascii(0, 4) === 'RIFF' && ascii(8, 12) === 'WEBP')
51
+ return 'webp';
52
+ if (ascii(0, 4) === 'RIFF' && ascii(8, 12) === 'WAVE')
53
+ return 'wav';
54
+ if (ascii(0, 3) === 'GIF')
55
+ return 'gif';
56
+ if (ascii(4, 8) === 'ftyp')
57
+ return 'mp4';
58
+ if (buf[0] === 0x1a && buf[1] === 0x45 && buf[2] === 0xdf && buf[3] === 0xa3)
59
+ return 'webm';
60
+ if (ascii(0, 3) === 'ID3' || (buf[0] === 0xff && (buf[1] & 0xe0) === 0xe0))
61
+ return 'mp3';
62
+ return undefined;
63
+ }
64
+ /** Extensions that name the same format, so a .jpeg holding JPEG bytes is right. */
65
+ const EXT_ALIASES = { jpeg: 'jpg', htm: 'html', yml: 'yaml' };
66
+ const canonicalExt = (ext) => EXT_ALIASES[ext] ?? ext;
67
+ /** The format's name, for the note — "JPEG", not the "JPG" of its extension. */
68
+ const formatName = (ext) => (canonicalExt(ext) === 'jpg' ? 'JPEG' : ext.toUpperCase());
69
+ /** Save under an extension that matches the bytes. A file whose extension lies
70
+ * about its format is not a cosmetic problem: `page eval --camera` validates and
71
+ * dispatches on the extension, image tools and browsers sniff it, and an agent
72
+ * that Reads a "*.png" full of JPEG bytes has to stop and work out which one is
73
+ * lying. The caller's -o path is a request, not a fact — honour its directory and
74
+ * stem, and let the bytes name the format. */
75
+ function correctExtension(filename, buf) {
76
+ const actual = sniffExt(buf);
77
+ if (!actual)
78
+ return filename; // unrecognized bytes — nothing better to say
79
+ const dot = filename.lastIndexOf('.');
80
+ const asked = dot > 0 ? filename.slice(dot + 1).toLowerCase() : '';
81
+ if (canonicalExt(asked) === actual)
82
+ return filename;
83
+ const corrected = `${dot > 0 ? filename.slice(0, dot) : filename}.${actual}`;
84
+ console.error(muted(`Note: the model returned ${formatName(actual)}, not ${asked ? formatName(asked) : 'the requested format'} — `
85
+ + `saving as ${corrected} so the extension matches the actual bytes.`));
86
+ return corrected;
87
+ }
34
88
  /** Create the parent directory of an -o path, so `-o src/audio/ding.mp3` works
35
89
  * in a tree that has no `src/audio/` yet instead of dying on a raw ENOENT.
36
90
  *
@@ -114,10 +168,13 @@ Examples:
114
168
  else {
115
169
  const sizeKb = Math.round(result.size_bytes / 1024);
116
170
  console.log(`${muted(`Generated with ${result.provider}/${result.model} (${sizeKb}KB)`)}`);
117
- console.log(success(`Saved to ${savedPath}`));
118
171
  if (result.seed !== undefined) {
119
172
  console.log(muted(`Seed ${result.seed} — pass --seed ${result.seed} to keep the next image coherent`));
120
173
  }
174
+ // Saved-path last: a caller reading a truncated tail of this output (an
175
+ // agent batching several generates in one shell call) must still see
176
+ // WHERE the bytes landed - that is the one line the next command needs.
177
+ console.log(success(`Saved to ${savedPath}`));
121
178
  }
122
179
  }
123
180
  catch (err) {
@@ -4,7 +4,7 @@ import { existsSync, readFileSync } from 'fs';
4
4
  import { getAccountSlug } from '../api.js';
5
5
  import { getConfig, getConfigPath, saveConfigAt } from '../config.js';
6
6
  import { getAuth } from '../auth.js';
7
- import { slugify, setupClaudeHooks, setupGitignore, SUPPORTED_TOOLS, DEFAULT_TOOLS, DEFAULT_SYNC_IGNORE } from '../setup.js';
7
+ import { slugify, setupProjectTools, SUPPORTED_TOOLS, DEFAULT_TOOLS, DEFAULT_SYNC_IGNORE } from '../setup.js';
8
8
  import { success, error as clrError, info, muted, bold } from '../colors.js';
9
9
  import { confirm } from '../utils.js';
10
10
  import { scanForAdoption, adoptCurrentDir, canAdoptCwd, formatBytes, formatCwdLabel, ADOPT_THRESHOLDS, } from '../adopt-cwd.js';
@@ -23,7 +23,7 @@ function resolveTools(forFlag) {
23
23
  }
24
24
  export const initCommand = new Command('init')
25
25
  .description('Link this directory to a project')
26
- .addHelpText('after', '\nWrites CLAUDE.md/AGENTS.md primer files so your AI coding tool understands Gipity.')
26
+ .addHelpText('after', '\nWrites CLAUDE.md/AGENTS.md primer files so your AI coding tool understands Gipity, and installs the Gipity skills + file-sync hooks into the agent CLIs found on this machine (Claude Code, Codex, Grok).')
27
27
  .argument('[name]', 'Project name/slug (defaults to current directory name)')
28
28
  .option('--agent <guid>', 'Agent GUID to use')
29
29
  .option('--no-capture', 'Don\'t record Claude Code sessions in this directory to your Gipity project (sets captureHooks: false in .gipity.json)')
@@ -32,7 +32,8 @@ export const initCommand = new Command('init')
32
32
  Examples:
33
33
  $ gipity init Link cwd as a new project (slug = dir name).
34
34
  $ gipity init my-app Link cwd with an explicit slug.
35
- $ gipity init --for codex Write only AGENTS.md (skip Claude/Cursor/etc).
35
+ $ gipity init --for codex AGENTS.md + Codex skills/sync hooks only.
36
+ $ gipity init --for grok AGENTS.md + the Gipity plugin in Grok only.
36
37
  $ gipity init --for cursor,gemini Write only the Cursor + Gemini primers.
37
38
  $ gipity init --for aider AGENTS.md + a read: entry in .aider.conf.yml
38
39
  (aider auto-reads nothing, so it's opt-in).
@@ -57,10 +58,6 @@ Working with an existing Gipity project:
57
58
  process.exit(1);
58
59
  }
59
60
  const wantsClaude = tools.some(t => t.key === 'claude');
60
- const writeAllPrimers = () => {
61
- for (const t of tools)
62
- t.setup();
63
- };
64
61
  const primerSummary = tools.map(t => t.label).join(', ');
65
62
  try {
66
63
  // Check auth
@@ -77,12 +74,9 @@ Working with an existing Gipity project:
77
74
  if (existsSync(resolve(cwd, '.gipity.json'))) {
78
75
  const existing = getConfig();
79
76
  console.log(`Already linked to ${info(`"${existing?.projectSlug ?? ''}"`)} ${muted(`(${existing?.projectGuid ?? ''})`)}`);
80
- // Re-run setup in case hooks/skills are missing. Claude Code hooks
81
- // only matter when the Claude primer is being written.
82
- if (wantsClaude)
83
- setupClaudeHooks();
84
- writeAllPrimers();
85
- setupGitignore();
77
+ // Re-run setup in case primers/hooks/skills are missing - each tool's
78
+ // integration self-gates on its binary and current-version state.
79
+ setupProjectTools(tools);
86
80
  // The config's ignore list was frozen at link time, so a workstation
87
81
  // artifact introduced by a newer CLI (e.g. aider's .aider.conf.yml)
88
82
  // would sync up as project content. Union in the current defaults.
@@ -143,7 +137,7 @@ Working with an existing Gipity project:
143
137
  const sizeStr = scan.truncated ? `>${formatBytes(ADOPT_THRESHOLDS.REFUSE_BYTES)}` : formatBytes(scan.bytes);
144
138
  const fileStr = scan.truncated ? `>${ADOPT_THRESHOLDS.REFUSE_FILES}` : `${scan.files}`;
145
139
  console.error(clrError(`Directory has ${fileStr} files (${sizeStr}) - too large to adopt as a Gipity project.`));
146
- console.error(muted('Move into a subdirectory, or use `gipity claude` and pick "Create new project".'));
140
+ console.error(muted('Move into a subdirectory, or use `gipity build` and pick "Create new project".'));
147
141
  process.exit(1);
148
142
  }
149
143
  if (scan.tier === 'moderate') {
@@ -183,7 +177,7 @@ Working with an existing Gipity project:
183
177
  }
184
178
  console.log(success(`Wrote primer files: ${primerSummary}.`));
185
179
  if (wantsClaude) {
186
- console.log(success('Ready! Run `gipity claude` for Claude Code, or open this directory in your other AI coding tool.'));
180
+ console.log(success('Ready! Run your coding agent here (claude, codex, grok), or `gipity build` to launch one with a picker.'));
187
181
  // Recording happens by default (however Claude Code is launched), so
188
182
  // say so up front - consent should be explicit, not discovered later.
189
183
  if (opts.capture === false) {