gipity 1.0.417 → 1.0.419

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.
@@ -148,6 +148,17 @@ function formatElapsed(ms) {
148
148
  * truncate the file. Best-effort: a malformed or unreadable config is left
149
149
  * untouched and never blocks the launch. Headless `-p` runs skip the dialog
150
150
  * already, so this is only needed for interactive launches. */
151
+ /** True when the Claude Code argv asks for stream-json output (both the
152
+ * `--output-format stream-json` and `--output-format=stream-json` forms).
153
+ * Combined with an inherited GIPITY_CONVERSATION_GUID this identifies a
154
+ * relay-daemon spawn, where the daemon owns capture and lifecycle-hook
155
+ * capture must stand down. Exported for tests. */
156
+ export function hasStreamJsonFlag(args) {
157
+ const idx = args.indexOf('--output-format');
158
+ if (idx !== -1 && args[idx + 1] === 'stream-json')
159
+ return true;
160
+ return args.includes('--output-format=stream-json');
161
+ }
151
162
  export function markFolderTrusted(dir) {
152
163
  try {
153
164
  const file = join(homedir(), '.claude.json');
@@ -675,12 +686,15 @@ export const claudeCommand = new Command('claude')
675
686
  // we can't satisfy the claude_code ownership rule, so hooks stay
676
687
  // offline for this run.
677
688
  //
678
- // Note: when `--output-format stream-json` is present in argv, the
679
- // relay daemon is capturing Claude's stdout directly and the hook-
680
- // based transcript capture would double-post every event. In that
681
- // case we intentionally do NOT propagate GIPITY_CONVERSATION_GUID
682
- // to the Claude child (see childEnv assignment below) - the hook's
683
- // existing "no guid silent no-op" guard then skips capture.
689
+ // Note: when this process was spawned BY the relay daemon (inherited
690
+ // GIPITY_CONVERSATION_GUID) with `--output-format stream-json`, the
691
+ // daemon is capturing Claude's stdout directly and hook-based capture
692
+ // would double-post every event. In that case we set GIPITY_CAPTURE=off
693
+ // on the Claude child (see childEnv assignment below) so the hook
694
+ // runner stands down. A USER-supplied stream-json run has no daemon
695
+ // behind it - hooks stay armed there, or the conversation would sit
696
+ // empty forever (the qwen-flight zero-message bug, 2026-07-07).
697
+ const inheritedConvGuid = Boolean(process.env.GIPITY_CONVERSATION_GUID);
684
698
  let convGuidForHooks = process.env.GIPITY_CONVERSATION_GUID ?? null;
685
699
  if (!convGuidForHooks) {
686
700
  const device = relayState.getDevice();
@@ -819,14 +833,16 @@ export const claudeCommand = new Command('claude')
819
833
  // throws EINVAL on Node >=18.20.2/20.12.2 - the CVE-2024-27980 fix).
820
834
  const claudeCmd = resolveCommand('claude');
821
835
  const childEnv = { ...process.env };
822
- // Gate hook-based capture: when the daemon is streaming via
823
- // --output-format stream-json, it owns the capture and any hook
824
- // post would be a dupe. Leaving GIPITY_CONVERSATION_GUID unset in
825
- // the child's env trips the hook runner's early-return guard.
826
- const streamJsonIdx = allArgs.indexOf('--output-format');
827
- const daemonCapturing = streamJsonIdx !== -1 && allArgs[streamJsonIdx + 1] === 'stream-json';
836
+ // Gate hook-based capture: only when the DAEMON is streaming via
837
+ // --output-format stream-json (inherited guid = daemon spawn) does it
838
+ // own the capture; any hook post would then be a dupe. GIPITY_CAPTURE=off
839
+ // stands the hook runner down explicitly - merely unsetting the guid is
840
+ // no longer enough now that the runner self-arms from .gipity.json.
841
+ // A user-supplied stream-json run (no inherited guid) keeps hooks armed.
842
+ const daemonCapturing = inheritedConvGuid && hasStreamJsonFlag(allArgs);
828
843
  if (daemonCapturing) {
829
844
  delete childEnv.GIPITY_CONVERSATION_GUID;
845
+ childEnv.GIPITY_CAPTURE = 'off';
830
846
  }
831
847
  else if (convGuidForHooks) {
832
848
  childEnv.GIPITY_CONVERSATION_GUID = convGuidForHooks;
@@ -274,11 +274,61 @@ Examples:
274
274
  process.exit(1);
275
275
  }
276
276
  });
277
+ // ── SOUND ──────────────────────────────────────────────────────────────
278
+ const soundCommand = new Command('sound')
279
+ .description(`Generate a sound effect from a text description using AI.
280
+
281
+ Always billed to you (the caller) - the app's service billing_mode does not
282
+ apply to direct generation, so there is never a reason to flip a service to
283
+ owner_pays just to create assets.
284
+
285
+ Tips:
286
+ - Describe the sound, not a scene (e.g. "cartoon boing with a springy wobble")
287
+ - Omit --duration to let the model pick a natural length (billing is per second)
288
+ - --influence closer to 1 follows the text more literally
289
+
290
+ Examples:
291
+ gipity generate sound "cartoon character saying oof"
292
+ gipity generate sound "thunder rolling in the distance" --duration 5
293
+ gipity generate sound "sci-fi laser blast" -o src/assets/sounds/laser.mp3`)
294
+ .argument('<text>', 'Text description of the sound effect (max 1000 characters)')
295
+ .option('--duration <seconds>', 'Clip length in seconds (0.5-30; default: automatic)', (v) => parseFloat(v))
296
+ .option('--influence <n>', 'How closely to follow the prompt, 0.0-1.0 (higher = more literal)', (v) => parseFloat(v))
297
+ .option('-o, --output <file>', 'Output path (default ./sound.mp3). For audio your app ships, write it into the source tree so it deploys, e.g. -o src/assets/sounds/boing.mp3; the cwd default is fine for one-off generation.')
298
+ .option('--json', 'Output as JSON')
299
+ .action(async (text, opts) => {
300
+ try {
301
+ const { config } = await resolveProjectContext();
302
+ const doGenerate = () => post(`/projects/${config.projectGuid}/generate/sound`, {
303
+ text,
304
+ duration_seconds: opts.duration,
305
+ prompt_influence: opts.influence,
306
+ });
307
+ const result = opts.json
308
+ ? await doGenerate()
309
+ : await withSpinner('Generating sound effect…', doGenerate, { done: null });
310
+ const filename = opts.output || 'sound.mp3';
311
+ const savedPath = await downloadFile(result.url, filename);
312
+ if (opts.json) {
313
+ console.log(JSON.stringify({ ...result, saved: savedPath }));
314
+ }
315
+ else {
316
+ const sizeKb = Math.round(result.size_bytes / 1024);
317
+ console.log(`${muted(`Generated sound effect (${sizeKb}KB)`)}`);
318
+ console.log(success(`Saved to ${savedPath}`));
319
+ }
320
+ }
321
+ catch (err) {
322
+ printCommandError('Sound generation', err);
323
+ process.exit(1);
324
+ }
325
+ });
277
326
  // ── PARENT COMMAND ─────────────────────────────────────────────────────
278
327
  export const generateCommand = new Command('generate')
279
- .description('Generate images, video, speech, or music')
328
+ .description('Generate images, video, speech, sound effects, or music')
280
329
  .addCommand(imageCommand)
281
330
  .addCommand(videoCommand)
282
331
  .addCommand(speechCommand)
332
+ .addCommand(soundCommand)
283
333
  .addCommand(musicCommand);
284
334
  //# sourceMappingURL=generate.js.map
@@ -1,6 +1,6 @@
1
1
  import { Command } from 'commander';
2
2
  import { basename, resolve, dirname } from 'path';
3
- import { existsSync } from 'fs';
3
+ 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';
@@ -26,6 +26,7 @@ export const initCommand = new Command('init')
26
26
  .addHelpText('after', '\nWrites CLAUDE.md/AGENTS.md primer files so your AI coding tool understands Gipity.')
27
27
  .argument('[name]', 'Project name/slug (defaults to current directory name)')
28
28
  .option('--agent <guid>', 'Agent GUID to use')
29
+ .option('--no-capture', 'Don\'t record Claude Code sessions in this directory to your Gipity project (sets captureHooks: false in .gipity.json)')
29
30
  .option('--for <tools>', `Which AI tool primer files to write (comma-separated). Default: all except aider (opt-in - it also writes .aider.conf.yml). Choices: ${TOOL_KEYS.join(', ')}, all`)
30
31
  .addHelpText('after', `
31
32
  Examples:
@@ -86,14 +87,28 @@ Working with an existing Gipity project:
86
87
  // artifact introduced by a newer CLI (e.g. aider's .aider.conf.yml)
87
88
  // would sync up as project content. Union in the current defaults.
88
89
  if (existing) {
90
+ let changed = false;
89
91
  const cur = existing.ignore ?? (existing.ignore = []);
90
92
  const missing = DEFAULT_SYNC_IGNORE.filter(e => !cur.includes(e));
91
93
  if (missing.length) {
92
94
  cur.push(...missing);
93
- saveConfigAt(cwd, existing);
95
+ changed = true;
96
+ }
97
+ // `--no-capture` on a re-init is the documented way to opt an
98
+ // existing project out of session recording. One-way from flags:
99
+ // a bare re-run never silently reverses an explicit opt-out
100
+ // (delete the key or set it true in .gipity.json to re-enable).
101
+ if (opts.capture === false && existing.captureHooks !== false) {
102
+ existing.captureHooks = false;
103
+ changed = true;
94
104
  }
105
+ if (changed)
106
+ saveConfigAt(cwd, existing);
95
107
  }
96
108
  console.log(success(`Refreshed primer files: ${primerSummary}.`));
109
+ if (opts.capture === false) {
110
+ console.log(success('Session recording disabled for this project (captureHooks: false in .gipity.json).'));
111
+ }
97
112
  return;
98
113
  }
99
114
  // Refuse $HOME / system roots - adopting one as a project root would
@@ -156,9 +171,27 @@ Working with an existing Gipity project:
156
171
  }
157
172
  if (adopted.applied > 0)
158
173
  console.log(`Synced ${adopted.applied} change${adopted.applied > 1 ? 's' : ''} with Gipity.`);
174
+ // Session recording opt-out. Written after adopt so it lands in the
175
+ // freshly created .gipity.json regardless of how the link happened.
176
+ if (opts.capture === false) {
177
+ try {
178
+ const cfg = JSON.parse(readFileSync(resolve(cwd, '.gipity.json'), 'utf-8'));
179
+ cfg.captureHooks = false;
180
+ saveConfigAt(cwd, cfg);
181
+ }
182
+ catch { /* config missing/unreadable - nothing to opt out of */ }
183
+ }
159
184
  console.log(success(`Wrote primer files: ${primerSummary}.`));
160
185
  if (wantsClaude) {
161
186
  console.log(success('Ready! Run `gipity claude` for Claude Code, or open this directory in your other AI coding tool.'));
187
+ // Recording happens by default (however Claude Code is launched), so
188
+ // say so up front - consent should be explicit, not discovered later.
189
+ if (opts.capture === false) {
190
+ console.log(muted('Session recording is off for this project (captureHooks: false in .gipity.json).'));
191
+ }
192
+ else {
193
+ console.log(muted('Claude Code sessions here are recorded to your Gipity project (view at prompt.gipity.ai). Opt out: gipity init --no-capture.'));
194
+ }
162
195
  }
163
196
  else {
164
197
  console.log(success('Ready! Open this directory in your AI coding tool.'));
@@ -13,7 +13,7 @@ const SERVICES = [
13
13
  { name: 'image/models', method: 'GET', desc: 'List image providers/models' },
14
14
  { name: 'tts', method: 'POST', desc: 'Text-to-speech ({ text, voice?, ... })' },
15
15
  { name: 'tts/voices', method: 'GET', desc: 'List TTS voices' },
16
- { name: 'sound', method: 'POST', desc: 'Sound effect ({ prompt, duration_seconds? })' },
16
+ { name: 'sound', method: 'POST', desc: 'Sound effect ({ text, duration_seconds? })' },
17
17
  { name: 'music', method: 'POST', desc: 'Music generation ({ prompt, duration_seconds? })' },
18
18
  { name: 'video', method: 'POST', desc: 'Video generation ({ prompt, ... })' },
19
19
  { name: 'location/ip', method: 'POST', desc: 'IP geolocation ({ ip? })' },
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * Internal hook runner - invoked by Claude Code's lifecycle hooks
4
4
  * (SessionStart, Stop, SubagentStop, SessionEnd, and a throttled
5
- * PostToolUse for mid-run flushing) to mirror a terminal `gipity claude`
5
+ * PostToolUse for mid-run flushing) to mirror a terminal Claude Code
6
6
  * session into the Gipity server so the web CLI can display it read-only.
7
7
  *
8
8
  * Not a user-facing `gipity` subcommand by design: users never invoke
@@ -16,9 +16,22 @@
16
16
  * source: 'claude-code' (today) | future: 'codex', …
17
17
  * event: 'session-start' | 'stop' | 'subagent-stop' | 'session-end' | 'post-tool-use' | 'pre-compact'
18
18
  *
19
+ * Conversation binding, in order:
20
+ * 1. GIPITY_CONVERSATION_GUID env var - set by `gipity claude`, which
21
+ * created/reused the conversation before spawning Claude Code.
22
+ * 2. A session_id → conv mapping persisted in the capture-state dir by
23
+ * an earlier event of this session.
24
+ * 3. Self-arm: the session was launched WITHOUT `gipity claude` (bare
25
+ * `claude` in a linked project dir). Resolve the project from
26
+ * .gipity.json and ask the server to bind this session_id to a
27
+ * conversation (POST /remote-sessions/resolve), then persist the
28
+ * mapping. This is what makes bare `claude` sessions record.
29
+ *
19
30
  * Graceful no-ops (exit 0 silently):
20
- * - GIPITY_CONVERSATION_GUID env var unset (hook fired from a bare
21
- * `claude`, not `gipity claude`).
31
+ * - GIPITY_CAPTURE=off - the relay daemon owns capture for this run
32
+ * (it parses stream-json from stdout), or the caller opted out.
33
+ * - No binding resolvable: not a Gipity project, `captureHooks: false`
34
+ * in .gipity.json, or the resolve call failed.
22
35
  * - The machine isn't paired - no device token available.
23
36
  * - Anything unexpected (parse error, network error, etc.). We must
24
37
  * not break the user's interactive session.
@@ -28,6 +41,7 @@ import { homedir } from 'os';
28
41
  import { join } from 'path';
29
42
  import { getDevice } from '../relay/state.js';
30
43
  import { deviceFetch } from '../relay/device-http.js';
44
+ import { getConfig } from '../config.js';
31
45
  import { parseTranscript, } from '../capture/sources/claude-code.js';
32
46
  const CAPTURE_DIR = join(homedir(), '.gipity', 'capture-state');
33
47
  const INGEST_BATCH_MAX = 100; // server caps at 200; stay comfortably under
@@ -159,6 +173,117 @@ export function acquireLock(convGuid) {
159
173
  }
160
174
  return null;
161
175
  }
176
+ // ─── conversation binding ────────────────────────────────────────────
177
+ // Sessions launched via `gipity claude` carry GIPITY_CONVERSATION_GUID.
178
+ // Bare `claude` in a linked project has no env binding, so the first
179
+ // event resolves one from the server (keyed by the agent session_id) and
180
+ // persists it here for every later event of the session.
181
+ /** session_id is Claude Code's session UUID - filesystem-safe by
182
+ * construction; the character guard keeps a hostile hook payload from
183
+ * escaping the capture-state dir (used for both the mapping file and
184
+ * the resolve lock). */
185
+ function safeSessionKey(sessionId) {
186
+ return `sid-${sessionId.replace(/[^A-Za-z0-9._-]/g, '_')}`;
187
+ }
188
+ function sessionMapPath(sessionId) {
189
+ return join(CAPTURE_DIR, `${safeSessionKey(sessionId)}.json`);
190
+ }
191
+ function readSessionMap(sessionId) {
192
+ try {
193
+ const parsed = JSON.parse(readFileSync(sessionMapPath(sessionId), 'utf-8'));
194
+ return typeof parsed.conv_guid === 'string' ? parsed.conv_guid : null;
195
+ }
196
+ catch {
197
+ return null;
198
+ }
199
+ }
200
+ function writeSessionMap(sessionId, convGuid) {
201
+ mkdirSync(CAPTURE_DIR, { recursive: true });
202
+ writeFileSync(sessionMapPath(sessionId), JSON.stringify({ conv_guid: convGuid }) + '\n');
203
+ }
204
+ function deleteSessionMap(sessionId) {
205
+ try {
206
+ unlinkSync(sessionMapPath(sessionId));
207
+ }
208
+ catch { /* already gone */ }
209
+ }
210
+ /** Ask the server which conversation this session belongs to: the conv
211
+ * already bound to this session_id (resume), the project's still-empty
212
+ * placeholder, or a fresh one. Returns null on any failure - capture is
213
+ * best-effort and must never break the session. */
214
+ async function resolveFromServer(projectGuid, hook) {
215
+ let res;
216
+ try {
217
+ res = await deviceFetch('POST', '/remote-sessions/resolve', {
218
+ project_guid: projectGuid,
219
+ session_id: hook.session_id,
220
+ cwd: hook.cwd,
221
+ }, 15_000);
222
+ }
223
+ catch {
224
+ return null;
225
+ }
226
+ if (!res.ok)
227
+ return null;
228
+ try {
229
+ const body = await res.json();
230
+ return body.data?.conversation_guid ?? null;
231
+ }
232
+ catch {
233
+ return null;
234
+ }
235
+ }
236
+ /** Resolve the conversation this event writes to. Env binding first (set
237
+ * by `gipity claude`), then the session's persisted mapping, then the
238
+ * self-arm server resolve. The resolve is serialized per-session with
239
+ * the same crash-safe lock the flushers use, so concurrent hooks (Stop +
240
+ * SubagentStop) can't each mint a conversation: the loser polls for the
241
+ * winner's mapping instead of racing the server. */
242
+ export async function resolveConvGuid(hook, sleep = (ms) => new Promise(r => setTimeout(r, ms))) {
243
+ const fromEnv = process.env.GIPITY_CONVERSATION_GUID;
244
+ if (fromEnv)
245
+ return fromEnv;
246
+ const sessionId = hook.session_id;
247
+ if (!sessionId)
248
+ return null;
249
+ const mapped = readSessionMap(sessionId);
250
+ if (mapped)
251
+ return mapped;
252
+ // Self-arm gates: linked project, capture not opted out, machine paired
253
+ // (main() already checked the device, but resolveConvGuid is also the
254
+ // unit-tested entry - keep it self-sufficient).
255
+ const config = getConfig();
256
+ if (!config?.projectGuid || config.captureHooks === false)
257
+ return null;
258
+ if (!getDevice())
259
+ return null;
260
+ const release = acquireLock(safeSessionKey(sessionId));
261
+ if (!release) {
262
+ // Another hook instance is resolving this session right now. Give it
263
+ // a moment and use its result; bail silently if it never lands (the
264
+ // next event retries the whole resolution).
265
+ for (let i = 0; i < 10; i++) {
266
+ await sleep(300);
267
+ const conv = readSessionMap(sessionId);
268
+ if (conv)
269
+ return conv;
270
+ }
271
+ return null;
272
+ }
273
+ try {
274
+ // Re-check under the lock - the previous holder may have just finished.
275
+ const raced = readSessionMap(sessionId);
276
+ if (raced)
277
+ return raced;
278
+ const conv = await resolveFromServer(config.projectGuid, hook);
279
+ if (conv)
280
+ writeSessionMap(sessionId, conv);
281
+ return conv;
282
+ }
283
+ finally {
284
+ release();
285
+ }
286
+ }
162
287
  async function readStdin() {
163
288
  if (process.stdin.isTTY)
164
289
  return '';
@@ -262,6 +387,8 @@ async function handleSessionEnd(convGuid, hook, source) {
262
387
  }];
263
388
  await postEntries(convGuid, entries);
264
389
  deleteState(convGuid);
390
+ if (hook.session_id)
391
+ deleteSessionMap(hook.session_id);
265
392
  }
266
393
  function displayName(source) {
267
394
  if (source === 'claude-code')
@@ -269,9 +396,12 @@ function displayName(source) {
269
396
  return source;
270
397
  }
271
398
  async function main() {
272
- const convGuid = process.env.GIPITY_CONVERSATION_GUID;
273
- if (!convGuid)
274
- return; // bare `claude`, not `gipity claude`
399
+ // Explicit capture opt-out. Set by the relay daemon's dispatch spawns
400
+ // (the daemon parses stream-json from stdout - hook capture would
401
+ // double-post every event) and available to anyone wanting a one-off
402
+ // unrecorded session.
403
+ if (process.env.GIPITY_CAPTURE === 'off')
404
+ return;
275
405
  if (!getDevice())
276
406
  return; // machine not paired
277
407
  const [source, event] = process.argv.slice(2);
@@ -285,6 +415,9 @@ async function main() {
285
415
  }
286
416
  catch { /* ignore - event may not require transcript */ }
287
417
  }
418
+ const convGuid = await resolveConvGuid(hook);
419
+ if (!convGuid)
420
+ return; // not a Gipity-bound session - nothing to capture
288
421
  try {
289
422
  switch (event) {
290
423
  case 'session-start':
package/dist/knowledge.js CHANGED
@@ -51,7 +51,7 @@ Prefer the cheapest option that works - CLI and sandbox are instant and free, ap
51
51
 
52
52
  1. CLI commands (fast, no agent overhead). The \`gipity\` CLI covers add, deploy, db, fn, logs, browser, sync, memory, skill, email, and more. All commands support \`--json\`. You can send email yourself - \`gipity email send\` goes out as the agent from \`gipity@gipity.ai\` with no setup or API keys (\`gipity skill read email\`); don't build a \`mailto:\` workaround or reach for an SMTP library.
53
53
  2. Cloud sandbox via \`gipity sandbox run\` - Docker container with pre-installed tools for media (ffmpeg, ImageMagick, sox), documents (pandoc, LibreOffice), and data (pandas, matplotlib, sqlite3). Run \`gipity skill read sandbox-tools\` for the full toolkit. No network from inside the sandbox - fetch what you need before sending it in.
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|music>\` or \`gipity chat\`. \`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.
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
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.
@@ -84,6 +84,8 @@ The one exception is app-level libraries the user imports into their own \`src/\
84
84
 
85
85
  ## Build loop
86
86
 
87
+ Prefer a live Gipity URL over an ephemeral artifact. When you'd otherwise reach for a Claude Code Artifact (or any host's built-in page preview) to show the user something, build it as a Gipity app and \`gipity deploy dev\` instead - the result is a real, shareable URL on their own project, not a throwaway.
88
+
87
89
  The full "when to add a template" rule and the definition of done are spelled out in the two sections at the end of this document. In short: if the user wants something deployable (web app, game, API), \`gipity add <template>\` first (default \`web-simple\`); for a one-off task (analysis, PDFs, data work), use \`gipity sandbox run\` instead; to add a reusable building block to an existing app (e.g. multiplayer), \`gipity add <kit>\`.
88
90
 
89
91
  Build loop: \`gipity add\` → edit files → \`gipity deploy dev\` → \`gipity page inspect <url>\` → fix any errors → repeat until the definition of done is met.
@@ -1233,7 +1233,12 @@ export async function spawnGipityClaude(args, cwd, d, queue, meta) {
1233
1233
  // feed the ephemeral live-typing channel (see stream-delta.ts); whole
1234
1234
  // assistant/tool events still arrive unchanged for the persistent path.
1235
1235
  const fullArgs = [...args, '--output-format', 'stream-json', '--verbose', '--include-partial-messages'];
1236
- const env = childEnv({ GIPITY_CONVERSATION_GUID: d.conversation_guid });
1236
+ // GIPITY_CAPTURE=off: the daemon owns capture for this dispatch (it
1237
+ // parses the stream-json on stdout), so the plugin's lifecycle-hook
1238
+ // capture must stand down. `gipity claude` sets the same sentinel on the
1239
+ // Claude child when it detects a daemon spawn; setting it here too keeps
1240
+ // dispatches double-post-free even across CLI version skew.
1241
+ const env = childEnv({ GIPITY_CONVERSATION_GUID: d.conversation_guid, GIPITY_CAPTURE: 'off' });
1237
1242
  return new Promise((resolve, reject) => {
1238
1243
  const child = spawnCommand(cmd, fullArgs, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
1239
1244
  // `exited` fires when the child fully unwinds (exit event). Callers
package/dist/setup.js CHANGED
@@ -145,7 +145,7 @@ export const LEGACY_MARKETPLACE_REPO = 'GipityAI/claude-plugin';
145
145
  // an installed plugin when the marketplace advances - only an explicit
146
146
  // `plugin install`/`update` does - so this constant is how a CLI upgrade tells
147
147
  // ensureGipityPluginInstalled() to refresh a stale user-scope install.
148
- export const GIPITY_PLUGIN_VERSION = '0.4.1';
148
+ export const GIPITY_PLUGIN_VERSION = '0.5.0';
149
149
  /** True for hook commands the CLI itself wrote into settings.json in past
150
150
  * versions. Matched by signature so migration strips exactly our own
151
151
  * entries and never touches user-authored hooks. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gipity",
3
- "version": "1.0.417",
3
+ "version": "1.0.419",
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",
@@ -13,7 +13,7 @@
13
13
  "postbuild": "node scripts/gen-build-info.mjs",
14
14
  "dev": "tsc --watch",
15
15
  "test": "npm run test:smoke",
16
- "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-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__/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__/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-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",
16
+ "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-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__/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-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",
17
17
  "test:smoke:quick": "node scripts/smoke-quick.mjs",
18
18
  "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",
19
19
  "test:e2e:sync": "tsc && GIPITY_E2E=1 node --test --test-timeout=180000 dist/__tests__/cli-e2e-sync-live.test.js",