gipity 1.1.5 → 1.1.7

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.
@@ -1,4 +1,4 @@
1
- import { Command } from 'commander';
1
+ import { Command, Option } from 'commander';
2
2
  import { get, post, put, del } from '../api.js';
3
3
  import { getConfig, requireConfig } from '../config.js';
4
4
  import { success, error as clrError, muted, bold } from '../colors.js';
@@ -29,6 +29,51 @@ function formatRunLine(r) {
29
29
  const statusColor = r.status === 'completed' ? success : r.status === 'failed' ? clrError : muted;
30
30
  return `${muted(r.short_guid)} ${statusColor(r.status)} ${dur} ${runTokens(r)} tokens ${muted(fmtTime(r.started_at))}`;
31
31
  }
32
+ /** Classify a step-output value that is a *string* holding JSON: parsed, or
33
+ * looks-like-JSON-but-doesn't-parse (a truncated / cut-off llm response — the
34
+ * step reports `completed` and the next step then can't read a field out of
35
+ * it). `null` means "not a JSON string at all". */
36
+ function jsonStringInfo(v) {
37
+ if (typeof v !== 'string')
38
+ return null;
39
+ const t = v.trim();
40
+ if (!t.startsWith('{') && !t.startsWith('['))
41
+ return null;
42
+ try {
43
+ const parsed = JSON.parse(t);
44
+ return typeof parsed === 'object' && parsed !== null ? { ok: true, value: parsed } : null;
45
+ }
46
+ catch {
47
+ return { ok: false };
48
+ }
49
+ }
50
+ function labelFor(info) {
51
+ return info.ok ? '(JSON string)' : clrError('(truncated/invalid JSON string)');
52
+ }
53
+ /** Render one step's output for humans.
54
+ *
55
+ * Step outputs routinely carry a JSON *string* under a key — an `llm` step's
56
+ * `result` is the common case. Dumped through a plain JSON.stringify that
57
+ * arrives as one enormous backslash-escaped line: unreadable, and it hides the
58
+ * single fact you need most, that the NEXT step sees a string, not an object
59
+ * (so `{{step.summary}}` resolves to nothing). Label such keys
60
+ * `"key" (JSON string)` and print the decoded value indented under it, so both
61
+ * the content and the string-vs-object handoff are obvious at a glance. */
62
+ function renderStepOutput(output) {
63
+ if (output && typeof output === 'object' && !Array.isArray(output)) {
64
+ return Object.entries(output).map(([k, v]) => {
65
+ const info = jsonStringInfo(v);
66
+ if (!info)
67
+ return `${JSON.stringify(k)}: ${JSON.stringify(v, null, 2)}`;
68
+ const body = info.ok ? JSON.stringify(info.value, null, 2) : JSON.stringify(v);
69
+ return `${JSON.stringify(k)} ${labelFor(info)}: ${body}`;
70
+ }).join('\n');
71
+ }
72
+ const info = jsonStringInfo(output);
73
+ if (info?.ok)
74
+ return JSON.stringify(info.value, null, 2);
75
+ return JSON.stringify(output, null, 2);
76
+ }
32
77
  /** Print each step's status, tokens, model, error and output. A run line alone
33
78
  * says a run finished, not what it did — without the steps you can't tell a
34
79
  * workflow that wrote a row from one that silently skipped every step. */
@@ -46,7 +91,7 @@ function printStepRuns(steps, emptyNote) {
46
91
  if (s.error_message)
47
92
  console.log(` ${clrError(s.error_message)}`);
48
93
  if (s.output_json !== null && s.output_json !== undefined) {
49
- console.log(JSON.stringify(s.output_json, null, 2).split('\n').map(l => ` ${l}`).join('\n'));
94
+ console.log(renderStepOutput(s.output_json).split('\n').map(l => ` ${l}`).join('\n'));
50
95
  }
51
96
  }
52
97
  }
@@ -149,10 +194,18 @@ workflowCommand
149
194
  }));
150
195
  workflowCommand
151
196
  .command('run <name>')
152
- .description('Trigger a workflow (add --wait to block until it finishes)')
197
+ .description("Run a workflow: waits for it to finish and prints each step's status and output (--no-wait to fire and forget)")
153
198
  .option('--json', 'Output as JSON')
154
- .option('--wait', 'Block until the triggered run reaches a terminal state, then print it')
155
- .option('--timeout <s>', 'Max seconds to wait with --wait', '120')
199
+ // Waiting is the default: a bare trigger tells you nothing about what the
200
+ // workflow did, so every caller followed it with a poll-and-drill-in loop.
201
+ // `--no-wait` is the escape hatch for fire-and-forget.
202
+ .option('--no-wait', 'Trigger and return immediately instead of waiting for the run')
203
+ // Waiting is already the default, but `--wait` reads naturally and older
204
+ // docs/muscle memory reach for it — accept it silently rather than erroring.
205
+ .addOption(new Option('--wait', 'Wait for the run (already the default)').hideHelp())
206
+ // LLM steps routinely take a couple of minutes; a 120s default timed out on
207
+ // healthy runs and taught callers to always pass --timeout.
208
+ .option('--timeout <s>', 'Max seconds to wait for the run to finish', '300')
156
209
  .action((name, _opts, cmd) => run('Run', async () => {
157
210
  const opts = mergedOpts(cmd);
158
211
  const wf = await resolveWorkflow(name);
@@ -168,7 +221,7 @@ workflowCommand
168
221
  const before = await get(`/workflows/${wf.short_guid}/runs?limit=1`);
169
222
  const prevGuid = before.data[0]?.short_guid;
170
223
  await post(`/workflows/${wf.short_guid}/run`, {});
171
- const r = await waitForRun(wf.short_guid, prevGuid, Number(opts.timeout) || 120);
224
+ const r = await waitForRun(wf.short_guid, prevGuid, Number(opts.timeout) || 300);
172
225
  if (opts.json) {
173
226
  console.log(JSON.stringify(r));
174
227
  }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Database checkpoint / restore — the undo for a LIVE write test.
3
+ *
4
+ * Verifying a write path for real (clicking Approve in the deployed UI, calling
5
+ * a function that inserts, letting a scheduled workflow run) mutates the
6
+ * project's real data, and there was no way to put it back - agents ended up
7
+ * hand-writing SQL to scrub their own test strings out.
8
+ *
9
+ * The snapshot itself is the SERVER's job: it lives in a sibling schema the app
10
+ * can't see, and restore runs in one transaction. This module is just the
11
+ * client - the CLI never composes checkpoint DDL, so a snapshot can never show
12
+ * up in `db query`, in a migration diff, or in app code that enumerates tables.
13
+ */
14
+ import { get, post } from './api.js';
15
+ /** First database in the project, or null when the project has none. */
16
+ export async function resolveDatabase(projectGuid, explicit) {
17
+ if (explicit)
18
+ return explicit;
19
+ const res = await get(`/projects/${projectGuid}/databases`);
20
+ return res.data.length > 0 ? res.data[0].friendlyName : null;
21
+ }
22
+ async function checkpoint(projectGuid, database, action, keep) {
23
+ const res = await post(`/projects/${projectGuid}/db/checkpoint`, { database, action, ...(keep ? { keep } : {}) });
24
+ return res.data;
25
+ }
26
+ /** Snapshot every app table. Replaces any previous checkpoint. */
27
+ export function createCheckpoint(projectGuid, database) {
28
+ return checkpoint(projectGuid, database, 'create');
29
+ }
30
+ /**
31
+ * Put every checkpointed table back to its snapshot contents and drop the
32
+ * snapshot (unless `keep`). Sequences are deliberately NOT rewound - leaving
33
+ * them ahead keeps ids unique against anything the test run handed out.
34
+ */
35
+ export function restoreCheckpoint(projectGuid, database, opts = {}) {
36
+ return checkpoint(projectGuid, database, 'restore', opts.keep);
37
+ }
38
+ /** Drop the checkpoint without restoring (keep whatever the run wrote). */
39
+ export function dropCheckpoint(projectGuid, database) {
40
+ return checkpoint(projectGuid, database, 'drop');
41
+ }
42
+ //# sourceMappingURL=db-checkpoint.js.map
@@ -51,6 +51,7 @@ import { getConfig } from '../config.js';
51
51
  import { parseTranscript as parseClaudeTranscript, } from '../capture/sources/claude-code.js';
52
52
  import { parseTranscript as parseCodexTranscript } from '../capture/sources/codex.js';
53
53
  import { parseTranscript as parseGrokTranscript } from '../capture/sources/grok.js';
54
+ import { parseTranscript as parseAgyTranscript } from '../capture/sources/agy.js';
54
55
  const CAPTURE_DIR = join(homedir(), '.gipity', 'capture-state');
55
56
  const INGEST_BATCH_MAX = 100; // server caps at 200; stay comfortably under
56
57
  export const CAPTURE_SOURCES = {
@@ -77,6 +78,14 @@ export const CAPTURE_SOURCES = {
77
78
  return join(homedir(), '.grok', 'sessions', encodeURIComponent(cwd), hook.session_id, 'chat_history.jsonl');
78
79
  },
79
80
  },
81
+ agy: {
82
+ serverSource: 'agy',
83
+ displayName: 'Antigravity',
84
+ // agy hook payloads always carry transcriptPath directly (no derivation
85
+ // needed, unlike Grok) - the agy-specific hook wrapper normalizes it into
86
+ // this HookInput's transcript_path before invoking this runner.
87
+ parse: (content, afterUuid, hook) => parseAgyTranscript(content, afterUuid, { conversationId: hook.session_id }),
88
+ },
80
89
  };
81
90
  // PostToolUse fires after every tool call. We flush on it so a session that is
82
91
  // killed/crashes mid-run (e.g. a long headless `gipity claude -p` build that
@@ -430,9 +439,10 @@ async function handleSessionEnd(convGuid, src, hook) {
430
439
  if (hook.session_id)
431
440
  deleteSessionMap(hook.session_id);
432
441
  }
433
- // Codex has no SessionEnd hook, so its capture-state/session-map files are
434
- // never cleaned by handleSessionEnd. Sweep anything stale on the way through
435
- // - the files are tiny, so a generous TTL is fine; a live session's state is
442
+ // Codex and agy have no SessionEnd-equivalent hook, so their capture-state/
443
+ // session-map files are never cleaned by handleSessionEnd. Sweep anything
444
+ // stale on the way through - the files are tiny, so a generous TTL is fine; a
445
+ // live session's state is
436
446
  // rewritten on every flush and never gets this old.
437
447
  const STATE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
438
448
  function sweepStaleState() {
@@ -455,8 +465,10 @@ function sweepStaleState() {
455
465
  }
456
466
  /** Normalize a raw hook payload to snake_case HookInput. Claude Code and
457
467
  * Codex deliver snake_case (`session_id`, `transcript_path`, `cwd`); Grok
458
- * Build delivers camelCase (`sessionId`, `hookEventName`, …). Accept both
459
- * so one runner serves every harness. */
468
+ * Build delivers camelCase (`sessionId`, `hookEventName`, …); agy calls its
469
+ * session id `conversationId` (its own hook payload has no session/cwd/event
470
+ * fields under any other name - see cli/src/agents/agy.ts). Accept all of
471
+ * these so one runner serves every harness. */
460
472
  export function normalizeHookInput(raw) {
461
473
  if (!raw || typeof raw !== 'object')
462
474
  return {};
@@ -469,7 +481,7 @@ export function normalizeHookInput(raw) {
469
481
  return undefined;
470
482
  };
471
483
  const hook = {
472
- session_id: pick('session_id', 'sessionId'),
484
+ session_id: pick('session_id', 'sessionId', 'conversationId'),
473
485
  transcript_path: pick('transcript_path', 'transcriptPath'),
474
486
  cwd: pick('cwd', 'workingDirectory'),
475
487
  hook_event_name: pick('hook_event_name', 'hookEventName'),
@@ -509,8 +521,8 @@ async function main() {
509
521
  if (derived)
510
522
  hook.transcript_path = derived;
511
523
  }
512
- // Opportunistic hygiene: Codex never fires session-end, so its state
513
- // files are TTL-swept instead of deleted at end-of-session.
524
+ // Opportunistic hygiene: Codex and agy never fire session-end, so their
525
+ // state files are TTL-swept instead of deleted at end-of-session.
514
526
  sweepStaleState();
515
527
  const convGuid = await resolveConvGuid(hook, src.serverSource);
516
528
  if (!convGuid)