gipity 1.1.5 → 1.1.6

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,5 +1,6 @@
1
1
  import { Command } from 'commander';
2
- import { get, post, put, patch, del } from '../api.js';
2
+ import { get, post, put, patch, del, publicRequest, mintAppToken } from '../api.js';
3
+ import { getAuth } from '../auth.js';
3
4
  import { requireConfig } from '../config.js';
4
5
  import { bold, muted } from '../colors.js';
5
6
  import { run, printList, printResult } from '../helpers/index.js';
@@ -10,6 +11,33 @@ import { confirm } from '../utils.js';
10
11
  // /projects/<guid>/records mirror.)
11
12
  export const recordsCommand = new Command('records')
12
13
  .description('Manage app records (Gipity Records - validated CRUD with audit history)');
14
+ const ANON_FLAG = '--anon';
15
+ const ANON_HELP = 'Call as an anonymous visitor (the public path a signed-out user hits) instead of as your signed-in account';
16
+ /** One HTTP surface for every records subcommand, so `--anon` is a persona
17
+ * switch rather than a different code path. Anonymous calls mint the same
18
+ * short-lived app token the browser SDK uses and send no owner credentials -
19
+ * this is exactly what a signed-out visitor's request looks like, so a 401 here
20
+ * is the true answer for an auth-gated table. The persona goes to stderr so
21
+ * stdout stays parseable; without it, "verify the anonymous path" silently runs
22
+ * as the owner and the public path never gets exercised. */
23
+ async function recordsHttp(opts) {
24
+ const config = requireConfig();
25
+ if (!opts.anon) {
26
+ const who = getAuth()?.email;
27
+ console.error(muted(`Auth: calling as ${who ?? 'your signed-in account'} (the owner persona; use ${ANON_FLAG} for the public visitor path)`));
28
+ return { guid: config.projectGuid, get, post, put, patch, del };
29
+ }
30
+ console.error(muted('Auth: anonymous visitor (the public path a signed-out user hits)'));
31
+ const headers = await mintAppToken(config.projectGuid);
32
+ return {
33
+ guid: config.projectGuid,
34
+ get: (path) => publicRequest('GET', path, undefined, headers),
35
+ post: (path, body) => publicRequest('POST', path, body, headers),
36
+ put: (path, body) => publicRequest('PUT', path, body, headers),
37
+ patch: (path, body) => publicRequest('PATCH', path, body, headers),
38
+ del: (path, body) => publicRequest('DELETE', path, body, headers),
39
+ };
40
+ }
13
41
  recordsCommand
14
42
  .command('list')
15
43
  .description('List record tables')
@@ -57,9 +85,10 @@ recordsCommand
57
85
  .option('--limit <n>', 'Max rows', '20')
58
86
  .option('--offset <n>', 'Offset', '0')
59
87
  .option('--fields <fields>', 'Comma-separated column names')
88
+ .option(ANON_FLAG, ANON_HELP)
60
89
  .option('--json', 'Output as JSON')
61
90
  .action((table, opts) => run('Query', async () => {
62
- const config = requireConfig();
91
+ const api = await recordsHttp(opts);
63
92
  const params = new URLSearchParams();
64
93
  if (opts.filter)
65
94
  params.set('filter', opts.filter);
@@ -69,7 +98,7 @@ recordsCommand
69
98
  params.set('offset', opts.offset);
70
99
  if (opts.fields)
71
100
  params.set('fields', opts.fields);
72
- const res = await get(`/api/${config.projectGuid}/records/${table}?${params}`);
101
+ const res = await api.get(`/api/${api.guid}/records/${table}?${params}`);
73
102
  if (opts.json) {
74
103
  console.log(JSON.stringify(res));
75
104
  }
@@ -85,21 +114,26 @@ recordsCommand
85
114
  recordsCommand
86
115
  .command('get <table> <id>')
87
116
  .description('Get a record')
117
+ .option(ANON_FLAG, ANON_HELP)
88
118
  .option('--json', 'Output as JSON')
89
119
  .action((table, id, opts) => run('Get', async () => {
90
- const config = requireConfig();
91
- const res = await get(`/api/${config.projectGuid}/records/${table}/${id}`);
120
+ const api = await recordsHttp(opts);
121
+ const res = await api.get(`/api/${api.guid}/records/${table}/${id}`);
92
122
  console.log(opts.json ? JSON.stringify(res.data) : JSON.stringify(res.data, null, 2));
93
123
  }));
94
124
  recordsCommand
95
- .command('history <table> <id>')
96
- .description('Audit history for a record (who/what changed it, with English summaries)')
125
+ .command('history <table> [id]')
126
+ .description('Audit history (who/what changed it, with English summaries). Omit <id> for the whole table\'s feed.')
97
127
  .option('--limit <n>', 'Max events', '20')
128
+ .option(ANON_FLAG, ANON_HELP)
98
129
  .option('--json', 'Output as JSON')
99
130
  .action((table, id, opts) => run('History', async () => {
100
- const config = requireConfig();
101
- const res = await get(`/api/${config.projectGuid}/records/${table}/${id}/history?limit=${encodeURIComponent(opts.limit)}`);
102
- printList(res.data, opts, 'No history for this record.', e => {
131
+ const api = await recordsHttp(opts);
132
+ // Table-wide feed when no id is given - the same endpoint an activity/history
133
+ // view reads, so verifying it needs no hand-built HTTP call.
134
+ const scope = id ? `${table}/${encodeURIComponent(id)}` : table;
135
+ const res = await api.get(`/api/${api.guid}/records/${scope}/history?limit=${encodeURIComponent(opts.limit)}`);
136
+ printList(res.data, opts, id ? 'No history for this record.' : `No history for "${table}".`, e => {
103
137
  const summary = e.detail?.summary || `${e.action} ${e.entity_type} ${e.entity_id}`;
104
138
  return `${muted(e.created_at)} ${bold(e.source || '-')} ${summary}`;
105
139
  });
@@ -108,34 +142,38 @@ recordsCommand
108
142
  .command('create <table>')
109
143
  .description('Create a record')
110
144
  .requiredOption('--data <json>', 'JSON object with field values')
145
+ .option(ANON_FLAG, ANON_HELP)
111
146
  .option('--json', 'Output as JSON')
112
147
  .action((table, opts) => run('Create', async () => {
113
- const config = requireConfig();
148
+ const api = await recordsHttp(opts);
114
149
  const data = JSON.parse(opts.data);
115
- const res = await post(`/api/${config.projectGuid}/records/${table}`, data);
150
+ const res = await api.post(`/api/${api.guid}/records/${table}`, data);
116
151
  printResult(`Created: ${JSON.stringify(res.data)}`, opts, res.data);
117
152
  }));
118
153
  recordsCommand
119
154
  .command('update <table> <id>')
120
155
  .description('Update a record')
121
156
  .requiredOption('--data <json>', 'JSON object with fields to update')
157
+ .option(ANON_FLAG, ANON_HELP)
122
158
  .option('--json', 'Output as JSON')
123
159
  .action((table, id, opts) => run('Update', async () => {
124
- const config = requireConfig();
160
+ const api = await recordsHttp(opts);
125
161
  const data = JSON.parse(opts.data);
126
- const res = await put(`/api/${config.projectGuid}/records/${table}/${id}`, data);
162
+ const res = await api.put(`/api/${api.guid}/records/${table}/${id}`, data);
127
163
  printResult(`Updated: ${JSON.stringify(res.data)}`, opts, res.data);
128
164
  }));
129
165
  recordsCommand
130
166
  .command('delete <table> <id>')
131
167
  .description('Delete a record')
132
- .action((table, id) => run('Delete', async () => {
168
+ .option(ANON_FLAG, ANON_HELP)
169
+ .option('--json', 'Output as JSON')
170
+ .action((table, id, opts) => run('Delete', async () => {
133
171
  if (!await confirm(`Delete record ${id} from "${table}"?`)) {
134
- console.log('Cancelled.');
172
+ printResult('Cancelled.', opts, { table, id, deleted: false, cancelled: true });
135
173
  return;
136
174
  }
137
- const config = requireConfig();
138
- await del(`/api/${config.projectGuid}/records/${table}/${id}`);
139
- printResult('Deleted.', { json: false });
175
+ const api = await recordsHttp(opts);
176
+ const res = await api.del(`/api/${api.guid}/records/${table}/${id}`);
177
+ printResult('Deleted.', opts, res?.data ?? { table, id, deleted: true });
140
178
  }));
141
179
  //# sourceMappingURL=records.js.map
@@ -4,7 +4,12 @@ import { bold, muted, success, warning } from '../colors.js';
4
4
  import { run, printList } from '../helpers/index.js';
5
5
  export const tokenCommand = new Command('token')
6
6
  .description('Manage API tokens')
7
- .addHelpText('after', '\nLong-lived agent API tokens (gip_at_*) for headless agents and CI.');
7
+ .addHelpText('after', `
8
+ Long-lived agent API tokens (gip_at_*) for headless agents and CI - they drive
9
+ the gipity CLI as YOU.
10
+
11
+ To let a script or cron write to one app instead, mint a project API key:
12
+ gipity key create "my script" --role editor (sent as X-Api-Key).`);
8
13
  const fmtDate = (d) => (d ? new Date(d).toLocaleDateString() : 'never');
9
14
  tokenCommand
10
15
  .command('create')
@@ -19,7 +19,7 @@ import { confirm, getAutoConfirm } from '../utils.js';
19
19
  import { bold, brand, dim, success, error as clrError, muted } from '../colors.js';
20
20
  import * as relayState from '../relay/state.js';
21
21
  import { planFor, UnsupportedPlatformError } from '../relay/installers.js';
22
- import { GIPITY_PLUGIN_ID, GIPITY_MARKETPLACE_NAME, stripGipityHooks, grokInstallState, agentSkillsState, AGENTS_SKILLS_DIR } from '../setup.js';
22
+ import { GIPITY_PLUGIN_ID, GIPITY_MARKETPLACE_NAME, stripGipityHooks, grokInstallState, agentSkillsState, AGENTS_SKILLS_DIR, agySkillsState, AGY_SKILLS_DIR, } from '../setup.js';
23
23
  /** Remove Gipity's entries from the user-scope Claude Code settings: the
24
24
  * plugin enablement, the marketplace registration, and any legacy hook
25
25
  * blocks older CLI versions wrote there. Surgical - everything else in the
@@ -240,6 +240,19 @@ export const uninstallCommand = new Command('uninstall')
240
240
  }
241
241
  console.log(`${success(`Removed ${agentSkills.skills.length} Gipity skills from ~/.agents/skills.`)}`);
242
242
  }
243
+ // 4c. Remove the skills the CLI copied into Antigravity's own global
244
+ // skill root (~/.gemini/config/skills - a different directory from
245
+ // Codex's ~/.agents/skills, see setup.ts).
246
+ const agySkills = agySkillsState();
247
+ if (agySkills.skills.length) {
248
+ for (const name of agySkills.skills) {
249
+ try {
250
+ rmSync(join(AGY_SKILLS_DIR, name), { recursive: true, force: true });
251
+ }
252
+ catch { /* best-effort */ }
253
+ }
254
+ console.log(`${success(`Removed ${agySkills.skills.length} Gipity skills from ~/.gemini/config/skills.`)}`);
255
+ }
243
256
  // 5. Wipe ~/.gipity/ (this also removes the agent-hooks scripts and the
244
257
  // agent-skills manifest, which live under it).
245
258
  if (existsSync(gipityDir)) {
@@ -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)