gipity 1.0.429 → 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/knowledge.js CHANGED
@@ -54,7 +54,7 @@ Prefer the cheapest option that works - CLI and sandbox are instant and free, ap
54
54
  3. App services - runtime HTTP endpoints your deployed app calls directly at \`https://a.gipity.ai/api/<PROJECT_GUID>/services/*\`. Available: LLM, TTS, image, sound, music, transcribe, video, file upload, realtime, location, push notifications (Gipity Notify - \`gipity add notify\`, send from a function with the injected \`notify()\`; see \`app-notify\`). Load the matching skill (\`app-llm\`, \`app-tts\`, etc.) before writing service code - they have the schemas, auth pattern, and common-mistake guards. For one-off generation during development, prefer \`gipity generate <image|video|speech|sound|music>\` or \`gipity chat\` - direct generation always bills you (the owner), regardless of any service \`billing_mode\` (that setting only governs the deployed app's runtime calls; never flip it just to create assets). \`gipity generate\` saves to a generic file in the current directory by default (e.g. \`./generated.png\`) - pass \`-o <path>\` to write it straight into your source tree so it deploys (e.g. \`gipity generate image "hero banner" -o src/assets/images/hero.png\`) instead of generating at cwd and moving it.
55
55
  4. Delegate to Gip (\`gipity chat "<task>"\`) - only when the work genuinely needs agent reasoning or a tool not in the CLI, sandbox, or app services. Required for: Twitter/X search, Gmail, calendar, push notifications, video understanding, audio source isolation, cross-model second opinions, multi-step orchestration. Don't use \`gipity chat\` for anything the sandbox can do - it's slower and burns tokens.
56
56
 
57
- You are the developer. Write files in this directory - the Gipity Claude Code plugin's hooks auto-sync them to Gipity. Don't run \`npm install\`, \`npm start\`, \`node\`, or \`python\` locally; there is no local runtime. Code runs in the Gipity sandbox.
57
+ You are the developer. Write files in this directory - Gipity's editor hooks (installed by \`gipity init\` into Claude Code, Grok, and Codex) auto-sync them to Gipity. Don't run \`npm install\`, \`npm start\`, \`node\`, or \`python\` locally; there is no local runtime. Code runs in the Gipity sandbox.
58
58
 
59
59
  ## Use first-party services before reaching outside
60
60
 
@@ -90,7 +90,7 @@ The full "when to add a template" rule and the definition of done are spelled ou
90
90
 
91
91
  Build loop: \`gipity add\` → edit files → \`gipity deploy dev --inspect\` → fix any errors → repeat until the definition of done is met. \`deploy --inspect\` deploys and then runs the page-inspect report on the live URL in one command; use a standalone \`gipity page inspect <url>\` only when re-checking without deploying.
92
92
 
93
- \`add\` writes real files to disk - Read a scaffolded file before your first Write/Edit to it, or the call fails \`"File has not been read yet"\`. Don't rewrite from memory of the template.
93
+ \`add\` writes real files to disk - Read a scaffolded file with the Read tool before your first Write/Edit to it, or the call fails \`"File has not been read yet"\` (viewing it with \`cat\`/\`head\` in Bash does not count - the harness only tracks the Read tool). Don't rewrite from memory of the template.
94
94
 
95
95
  Make your file changes and verify they landed, then run \`gipity deploy dev\` once. \`0 uploaded, N unchanged\` means nothing changed on disk - fix the files, don't re-run deploy or probe the environment.
96
96
 
@@ -146,7 +146,7 @@ Every tool call returns its full output with that call. There is no output buffe
146
146
 
147
147
  This directory is the app root - it holds \`.gipity.json\` (and \`gipity.yaml\` for backends) and is already your cwd, so run app commands here. Don't \`cd\` to a git root (\`git rev-parse --show-toplevel\`): when the app is nested in a larger repo that resolves outside the app.
148
148
 
149
- Write files locally - the Gipity Claude Code plugin's hooks auto-push every save to Gipity and auto-pull remote changes (images, audio from \`gipity chat\`) before each turn. Use \`gipity sync\` if things get out of sync (or if the plugin isn't installed - \`gipity status --repair-hooks\` re-enables it). Deletes are safe - use \`rollback\` with a datetime to undo, or \`file_version_restore\` for individual files.
149
+ Write files locally - Gipity's editor hooks (installed into Claude Code, Grok, and Codex by \`gipity init\`) auto-push every save to Gipity and auto-pull remote changes (images, audio from \`gipity chat\`) before each turn. Use \`gipity sync\` if things get out of sync (or if the hooks aren't installed - \`gipity status --repair-hooks\` re-enables them). Deletes are safe - use \`rollback\` with a datetime to undo, or \`file_version_restore\` for individual files.
150
150
 
151
151
  To keep local-only material (research clones, scratch data, vendored references) in the project directory without syncing or deploying it, list it in a \`.gipityignore\` at the project root - gitignore-style, one pattern per line, \`#\` comments. Ignored paths are invisible to sync in both directions; anything that already synced before being ignored stays on the server until you delete it.
152
152
 
package/dist/prefs.js ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Machine-level user preferences (~/.gipity/prefs.json) - the `gipity build`
3
+ * picker's memory. Deliberately NOT in a project's .gipity.json: per-project
4
+ * storage would re-ask the agent/model questions on every new project, which
5
+ * defeats "hit enter twice".
6
+ *
7
+ * Shape:
8
+ * {
9
+ * "lastAgent": "claude",
10
+ * "lastModel": { "claude": "opus", "codex": null } // per-agent so a
11
+ * } // Codex session never
12
+ * // clobbers the Claude pick
13
+ *
14
+ * A `lastModel` of null/absent means "Agent default" - launch passes no
15
+ * model flag and the agent's own model memory stays authoritative.
16
+ */
17
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
18
+ import { homedir } from 'os';
19
+ import { join } from 'path';
20
+ const PREFS_PATH = join(homedir(), '.gipity', 'prefs.json');
21
+ export function readPrefs() {
22
+ if (!existsSync(PREFS_PATH))
23
+ return {};
24
+ try {
25
+ const parsed = JSON.parse(readFileSync(PREFS_PATH, 'utf-8'));
26
+ return {
27
+ lastAgent: typeof parsed.lastAgent === 'string' ? parsed.lastAgent : undefined,
28
+ lastModel: parsed.lastModel && typeof parsed.lastModel === 'object' ? parsed.lastModel : undefined,
29
+ };
30
+ }
31
+ catch {
32
+ return {}; // unreadable prefs must never block a launch
33
+ }
34
+ }
35
+ export function writePrefs(update) {
36
+ try {
37
+ const current = readPrefs();
38
+ const next = {
39
+ ...current,
40
+ ...update,
41
+ lastModel: { ...current.lastModel, ...update.lastModel },
42
+ };
43
+ mkdirSync(join(homedir(), '.gipity'), { recursive: true });
44
+ writeFileSync(PREFS_PATH, JSON.stringify(next, null, 2) + '\n');
45
+ }
46
+ catch {
47
+ /* best-effort - prefs are a convenience */
48
+ }
49
+ }
50
+ //# sourceMappingURL=prefs.js.map
@@ -7,7 +7,7 @@
7
7
  import { clearConfigCache, saveConfigAt, getApiBaseOverride, DEFAULT_API_BASE } from './config.js';
8
8
  import { sync } from './sync.js';
9
9
  import { createProgressReporter } from './progress.js';
10
- import { setupClaudeHooks, setupGitignore, DEFAULT_TOOLS, DEFAULT_SYNC_IGNORE } from './setup.js';
10
+ import { setupProjectTools, DEFAULT_TOOLS, DEFAULT_SYNC_IGNORE } from './setup.js';
11
11
  import { substituteDir } from './template-vars.js';
12
12
  import { muted } from './colors.js';
13
13
  /** Write `.gipity.json` in `dir`, chdir into it so the hook/skill writers
@@ -61,15 +61,7 @@ export async function finalizeLocalProject(opts) {
61
61
  throw err;
62
62
  // soft mode - swallow; caller can log
63
63
  }
64
- const tools = opts.tools ?? DEFAULT_TOOLS;
65
- // Claude hooks (file-sync + capture) only matter if the Claude primer is
66
- // being written. Skipping them for Codex/Gemini/Cursor-only inits avoids
67
- // littering `.claude/settings.json` into projects that don't use Claude.
68
- if (tools.some(t => t.key === 'claude'))
69
- setupClaudeHooks();
70
- for (const t of tools)
71
- t.setup();
72
- setupGitignore();
64
+ setupProjectTools(opts.tools ?? DEFAULT_TOOLS);
73
65
  return { applied };
74
66
  }
75
67
  //# sourceMappingURL=project-setup.js.map
@@ -6,7 +6,7 @@ import { homedir, hostname, platform as osPlatform, loadavg, freemem, totalmem,
6
6
  import { join } from 'path';
7
7
  import { getApiBaseOverride, DEFAULT_API_BASE } from '../config.js';
8
8
  import { getProjectsRoot } from './paths.js';
9
- import { setupClaudeHooks, setupClaudeMd, setupAgentsMd, setupGitignore, DEFAULT_SYNC_IGNORE } from '../setup.js';
9
+ import { setupProjectTools, DEFAULT_SYNC_IGNORE } from '../setup.js';
10
10
  import { getAuth, readAuthFresh, refreshTokenIfNeeded, accessTokenExpired } from '../auth.js';
11
11
  import { post } from '../api.js';
12
12
  import * as state from './state.js';
@@ -24,7 +24,8 @@ import { SessionPool, PoolFullError } from './session-pool.js';
24
24
  import { getConfig } from '../config.js';
25
25
  import { getAccountSlug } from '../api.js';
26
26
  import { buildFreshWrap, buildResumeWrap, buildProjectContextBlock } from '../prompts.js';
27
- import { fetchProjectStats } from '../commands/claude.js';
27
+ import { fetchProjectStats } from '../commands/build.js';
28
+ import { getAdapterBySource } from '../agents/index.js';
28
29
  // Re-exported so the existing `relay-bridge-abort.test.ts` keeps working.
29
30
  // New callers should import from device-http.js directly.
30
31
  export const bridgeAbort = bridgeAbortImpl;
@@ -949,6 +950,15 @@ async function handleDispatch(claimed) {
949
950
  if (!syncFailed)
950
951
  pushSystem(bootstrapped ? 'Project files synced.' : 'Attached files synced.');
951
952
  }
953
+ // Which agent runs this dispatch. Older servers omit remote_type - they
954
+ // only ever dispatched Claude Code.
955
+ const adapter = getAdapterBySource(d.remote_type ?? 'claude_code');
956
+ // Non-Claude agents take the simpler hook-capture dispatch path: no
957
+ // stream-json ingest, no session pool.
958
+ if (adapter.key !== 'claude') {
959
+ await handleNonClaudeDispatch(adapter, d, cwd, queue, pushSystem, flushQueue);
960
+ return;
961
+ }
952
962
  // Phase 2: if the session pool is enabled, try to run this turn in a
953
963
  // long-lived Claude Code session (fast follow-up + clean interrupt). Any
954
964
  // failure - pool saturated, SDK error, wrap failure - falls through to the
@@ -958,8 +968,11 @@ async function handleDispatch(claimed) {
958
968
  if (handled)
959
969
  return;
960
970
  }
961
- // Build argv for `gipity claude -p …` (or with --resume). No shell - argv
962
- // as array so the message string can't be interpreted as shell syntax.
971
+ // Build argv for `gipity build --agent claude -p …` (or with --resume).
972
+ // No shell - argv as array so the message string can't be interpreted as
973
+ // shell syntax. (`build` is the renamed launcher; the flags below pass
974
+ // through it to the `claude` binary unchanged, exactly as the legacy
975
+ // `gipity claude -p` spawn did.)
963
976
  //
964
977
  // `--permission-mode bypassPermissions`: a relay dispatch has no
965
978
  // human on the other end to click "Approve" - Claude prompting would
@@ -967,7 +980,7 @@ async function handleDispatch(claimed) {
967
980
  // the device and dispatching the message; skipping the interactive
968
981
  // prompt is correct (same authority as running `claude -p` in a local
969
982
  // terminal yourself).
970
- const args = ['claude', '-p', d.message, '--permission-mode', 'bypassPermissions'];
983
+ const args = ['build', '--agent', 'claude', '-p', d.message, '--permission-mode', 'bypassPermissions'];
971
984
  // Relay sessions run in -p mode, where Claude Code's AskUserQuestion tool
972
985
  // is unavailable - so without help the model would ask clarifying
973
986
  // questions as prose the user just types back. This system-prompt
@@ -984,7 +997,7 @@ async function handleDispatch(claimed) {
984
997
  if (d.kind === 'resume' && d.remote_session_id) {
985
998
  args.push('--resume', d.remote_session_id);
986
999
  }
987
- log('debug', 'spawning gipity claude', {
1000
+ log('debug', 'spawning gipity build --agent claude', {
988
1001
  id: d.short_guid,
989
1002
  cwd,
990
1003
  args,
@@ -1123,7 +1136,92 @@ async function handleDispatch(claimed) {
1123
1136
  }
1124
1137
  else {
1125
1138
  log('warn', 'dispatch child exited nonzero', { id: d.short_guid, exitCode, ms });
1126
- await ack(d.short_guid, 'error', `gipity claude exited with code ${exitCode}${stderrNote}`);
1139
+ await ack(d.short_guid, 'error', `Claude Code exited with code ${exitCode}${stderrNote}`);
1140
+ }
1141
+ }
1142
+ /**
1143
+ * Dispatch for non-Claude agents (Codex, Grok). Deliberately simpler than
1144
+ * the Claude path: there is no stream-json ingest mapper for these agents -
1145
+ * hook-based session capture (Phase C) owns the transcript. The spawned
1146
+ * `gipity build --agent <key> -p …` child gets GIPITY_CONVERSATION_GUID (so
1147
+ * capture posts into the right conversation) and capture is NOT turned off.
1148
+ * The daemon contributes: the "Running <agent>" marker, byte-level progress
1149
+ * heartbeats (spawnGipityClaude's generic plumbing with streamJson:false),
1150
+ * post-run file sync, the tail marker, and the ack. The prompt entry itself
1151
+ * arrives via capture from the agent's own transcript - the daemon doesn't
1152
+ * push one, or it would render twice.
1153
+ */
1154
+ async function handleNonClaudeDispatch(adapter, d, cwd, queue, pushSystem, flushQueue) {
1155
+ // `gipity build` translates these into the agent's own argv via the same
1156
+ // adapter (Codex: `exec …`; Grok: `-p … --always-approve`). Raw message -
1157
+ // AGENTS.md carries the Gipity context for these agents; no wrapping.
1158
+ const args = ['build', '--agent', adapter.key, '-p', d.message, '--bypass-approvals'];
1159
+ if (d.model)
1160
+ args.push('--model', d.model);
1161
+ if (d.kind === 'resume' && d.remote_session_id)
1162
+ args.push('--resume', d.remote_session_id);
1163
+ log('debug', `spawning gipity build --agent ${adapter.key}`, {
1164
+ id: d.short_guid, cwd, args, conv: d.conversation_guid,
1165
+ chain: d.kind === 'resume' ? `resume ${d.remote_session_id}` : 'start (fresh session)',
1166
+ });
1167
+ const words = d.message.trim().split(/\s+/).filter(Boolean).length;
1168
+ pushSystem(`Running ${adapter.displayName} - ${words.toLocaleString('en-US')} words`);
1169
+ const t0 = Date.now();
1170
+ let exitCode = 1;
1171
+ let spawnErr = null;
1172
+ let killed = false;
1173
+ let runtimeLimit = false;
1174
+ let stderrTail = '';
1175
+ try {
1176
+ const result = await spawnGipityClaude(args, cwd, d, queue, { streamJson: false });
1177
+ exitCode = result.exitCode;
1178
+ killed = result.killed;
1179
+ runtimeLimit = result.runtimeLimit ?? false;
1180
+ stderrTail = result.stderrTail ?? '';
1181
+ }
1182
+ catch (err) {
1183
+ spawnErr = err?.message || String(err);
1184
+ log('error', 'dispatch spawn failed', { id: d.short_guid, err: spawnErr });
1185
+ }
1186
+ const dur = formatDuration(Date.now() - t0);
1187
+ // Same post-run reconcile as the Claude path: push files the agent wrote
1188
+ // via Bash/scripts back to VFS before the ack (hooks only cover its
1189
+ // native edit tools). Skipped on kill - the replacement dispatch owns the tree.
1190
+ if (!spawnErr && !killed) {
1191
+ try {
1192
+ await spawnSync(cwd, PROJECT_SYNC_TIMEOUT_MS);
1193
+ }
1194
+ catch (err) {
1195
+ log('warn', 'sync after dispatch failed', { id: d.short_guid, err: err?.message });
1196
+ }
1197
+ }
1198
+ const stderrNote = stderrTail ? `: ${stderrTail.slice(0, 300)}` : '';
1199
+ const tail = runtimeLimit
1200
+ ? `stopped after ${dur} (runtime limit)`
1201
+ : killed
1202
+ ? `cancelled (${dur})`
1203
+ : spawnErr
1204
+ ? `failed (${dur}: ${spawnErr})`
1205
+ : exitCode === 0
1206
+ ? `finished (${dur})`
1207
+ : `failed (${dur}, exit ${exitCode}${stderrNote})`;
1208
+ pushSystem(`${adapter.displayName} ${tail}`);
1209
+ await flushQueue();
1210
+ if (runtimeLimit) {
1211
+ await ack(d.short_guid, 'error', `${adapter.displayName} stopped after ${dur} (runtime limit)`);
1212
+ }
1213
+ else if (killed) {
1214
+ await ack(d.short_guid, 'cancelled');
1215
+ }
1216
+ else if (spawnErr) {
1217
+ await ack(d.short_guid, 'error', spawnErr);
1218
+ }
1219
+ else if (exitCode === 0) {
1220
+ log('info', 'dispatch done', { id: d.short_guid, agent: adapter.key });
1221
+ await ack(d.short_guid, 'done');
1222
+ }
1223
+ else {
1224
+ await ack(d.short_guid, 'error', `${adapter.displayName} exited with code ${exitCode}${stderrNote}`);
1127
1225
  }
1128
1226
  }
1129
1227
  /**
@@ -1372,10 +1470,7 @@ async function resolveCwdForProject(d) {
1372
1470
  const origCwd = process.cwd();
1373
1471
  try {
1374
1472
  process.chdir(path);
1375
- setupClaudeHooks();
1376
- setupClaudeMd();
1377
- setupAgentsMd();
1378
- setupGitignore();
1473
+ setupProjectTools();
1379
1474
  }
1380
1475
  finally {
1381
1476
  process.chdir(origCwd);
@@ -1562,6 +1657,11 @@ async function spawnSync(cwd, timeoutMs, onSpawn) {
1562
1657
  });
1563
1658
  }
1564
1659
  export async function spawnGipityClaude(args, cwd, d, queue, meta) {
1660
+ // streamJson (default true) = the Claude path: parse the child's
1661
+ // stream-json stdout into ingest entries and stand hook capture down.
1662
+ // false = non-Claude agents (Codex/Grok): hook capture owns the
1663
+ // transcript; stdout only feeds the byte-level progress heartbeat.
1664
+ const streamJson = meta?.streamJson !== false;
1565
1665
  // resolveCommand: on Windows the bare `gipity` is a .cmd shim that spawn
1566
1666
  // can't launch without an explicit path. An explicit env override is used
1567
1667
  // verbatim (it may be a full path); only the default name is resolved.
@@ -1572,13 +1672,19 @@ export async function spawnGipityClaude(args, cwd, d, queue, meta) {
1572
1672
  // `--include-partial-messages` adds stream_event token deltas, which
1573
1673
  // feed the ephemeral live-typing channel (see stream-delta.ts); whole
1574
1674
  // assistant/tool events still arrive unchanged for the persistent path.
1575
- const fullArgs = [...args, '--output-format', 'stream-json', '--verbose', '--include-partial-messages'];
1675
+ const fullArgs = streamJson
1676
+ ? [...args, '--output-format', 'stream-json', '--verbose', '--include-partial-messages']
1677
+ : [...args];
1576
1678
  // GIPITY_CAPTURE=off: the daemon owns capture for this dispatch (it
1577
1679
  // parses the stream-json on stdout), so the plugin's lifecycle-hook
1578
1680
  // capture must stand down. `gipity claude` sets the same sentinel on the
1579
1681
  // Claude child when it detects a daemon spawn; setting it here too keeps
1580
- // dispatches double-post-free even across CLI version skew.
1581
- const env = childEnv({ GIPITY_CONVERSATION_GUID: d.conversation_guid, GIPITY_CAPTURE: 'off' });
1682
+ // dispatches double-post-free even across CLI version skew. Non-Claude
1683
+ // agents are the opposite: hook capture IS the transcript path, so the
1684
+ // sentinel must NOT be set.
1685
+ const env = streamJson
1686
+ ? childEnv({ GIPITY_CONVERSATION_GUID: d.conversation_guid, GIPITY_CAPTURE: 'off' })
1687
+ : childEnv({ GIPITY_CONVERSATION_GUID: d.conversation_guid });
1582
1688
  return new Promise((resolve, reject) => {
1583
1689
  const child = spawnCommand(cmd, fullArgs, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
1584
1690
  // `exited` fires when the child fully unwinds (exit event). Callers
@@ -1780,9 +1886,11 @@ export async function spawnGipityClaude(args, cwd, d, queue, meta) {
1780
1886
  child.stdout?.on('data', (chunk) => {
1781
1887
  stdoutBytesTotal += chunk.length;
1782
1888
  lastStdoutByteAt = Date.now();
1783
- splitter.push(chunk);
1889
+ if (streamJson)
1890
+ splitter.push(chunk);
1784
1891
  });
1785
- child.stdout?.on('end', () => splitter.flush());
1892
+ child.stdout?.on('end', () => { if (streamJson)
1893
+ splitter.flush(); });
1786
1894
  // Stderr: human-readable only (Claude's progress bars, errors).
1787
1895
  // Kept on the daemon's own stderr for `gipity relay log`, plus a
1788
1896
  // small tail ring so a crash with no stream output can include the
@@ -137,14 +137,16 @@ export async function collectDiagnostics() {
137
137
  } })();
138
138
  // Run the subprocess probes concurrently (each already bounded + best-effort)
139
139
  // so the whole snapshot costs one timeout, not the sum of four.
140
- const [claude, codex, cursor, gpu] = await Promise.all([
141
- probeVersion('claude'), probeVersion('codex'), probeVersion('cursor'), detectGpu(),
140
+ const [claude, codex, grok, cursor, gpu] = await Promise.all([
141
+ probeVersion('claude'), probeVersion('codex'), probeVersion('grok'), probeVersion('cursor'), detectGpu(),
142
142
  ]);
143
143
  const agents = {};
144
144
  if (claude)
145
145
  agents.claude_code = claude;
146
146
  if (codex)
147
147
  agents.codex = codex;
148
+ if (grok)
149
+ agents.grok = grok;
148
150
  if (cursor)
149
151
  agents.cursor = cursor;
150
152
  return {
@@ -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 {