gigarag-claude-code 0.1.1 → 0.2.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.
@@ -0,0 +1,159 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { join, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { isSea } from 'node:sea';
5
+ import { stateDir } from '../paths.js';
6
+ import { State } from '../state.js';
7
+ import { isClean } from '../git/git.js';
8
+ import { maxRewriteFiles, writeNote } from './notes.js';
9
+ import { syncSlug } from './slug.js';
10
+ import { RepoSyncRemote } from './remote.js';
11
+ import { claudeInvocation, findClaude, runRewriter } from './rewriter.js';
12
+ import { finishSync, holderFor, leaseMetaKey, runStage, WORKER_LOCK_MS as LOCK_MS } from './stage.js';
13
+ import { syncTarget } from './target.js';
14
+ const TICK_MS = 4 * 60_000;
15
+ const CHILD_TIMEOUT_MS = 40 * 60_000;
16
+ const quietMs = () => {
17
+ const n = Number(process.env['GIGARAG_SYNC_QUIET_MS']);
18
+ return Number.isFinite(n) && n >= 0 ? n : 20_000;
19
+ };
20
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
21
+ /** Starts the background worker for `root`, detached from whatever is calling this, and does not
22
+ * wait for it. Used by a git hook, and by `sync end`/`sync begin` when clearing the interactive
23
+ * marker frees a hook-mode sync that deferred behind it (its `deferred` note), so that commit
24
+ * gets synced once the marker is gone instead of waiting for another one to arrive. */
25
+ export function spawnWorker(root) {
26
+ const [cmd, args] = isSea()
27
+ ? [process.execPath, ['sync', 'worker', root]]
28
+ : [process.execPath, [fileURLToPath(new URL('../bin.js', import.meta.url)), 'sync', 'worker', root]];
29
+ spawn(cmd, args, { detached: true, stdio: 'ignore', windowsHide: true, cwd: root }).unref();
30
+ }
31
+ /**
32
+ * What a git hook runs. Records that a sync was asked for and starts the
33
+ * worker detached, then returns, so the hook costs one Node start in the
34
+ * background. Every failure ends as exit 0 and silence.
35
+ */
36
+ export async function hookEntry(_hook) {
37
+ try {
38
+ if (process.env['GIGARAG_NO_SYNC'] || process.env['GIGARAG_SYNC_WORKER'])
39
+ return 0;
40
+ const { root, git, slug } = syncSlug(process.cwd());
41
+ if (!git)
42
+ return 0;
43
+ const state = new State();
44
+ try {
45
+ state.setMeta(`sync_request:${slug}`, String(Date.now()));
46
+ }
47
+ finally {
48
+ state.close();
49
+ }
50
+ spawnWorker(root);
51
+ }
52
+ catch {
53
+ /* a hook never fails the git command that ran it */
54
+ }
55
+ return 0;
56
+ }
57
+ /**
58
+ * The background run. One per repository at a time (a local lock), started no
59
+ * sooner than the quiet period after the latest request, and run again if a
60
+ * request arrived while it worked, so a burst of commits costs one run.
61
+ */
62
+ export async function runWorker(root) {
63
+ const target = syncTarget(resolve(root));
64
+ const state = new State();
65
+ const lockKey = `sync_run:${target.slug}`;
66
+ try {
67
+ if (!state.tryAcquire(lockKey, LOCK_MS))
68
+ return 0;
69
+ for (;;) {
70
+ const requested = Number(state.getMeta(`sync_request:${target.slug}`) ?? 0);
71
+ const wait = requested + quietMs() - Date.now();
72
+ if (wait > 0) {
73
+ await sleep(wait);
74
+ continue;
75
+ }
76
+ const started = Date.now();
77
+ state.setMeta(lockKey, String(started));
78
+ try {
79
+ await syncOnce(state, target);
80
+ }
81
+ finally {
82
+ // Release before looking for another request. A hook that fires between the sync
83
+ // finishing and this check must find the lock free, or its request is lost: this
84
+ // run is about to decide there is nothing left to do, and nothing else would ever
85
+ // look again.
86
+ state.setMeta(lockKey, '0');
87
+ }
88
+ if (Number(state.getMeta(`sync_request:${target.slug}`) ?? 0) <= started)
89
+ return 0;
90
+ if (!state.tryAcquire(lockKey, LOCK_MS))
91
+ return 0;
92
+ }
93
+ }
94
+ catch (e) {
95
+ state.setMeta(lockKey, '0');
96
+ writeNote(state, target.slug, { kind: 'failed', detail: e.message });
97
+ return 1;
98
+ }
99
+ finally {
100
+ state.close();
101
+ }
102
+ }
103
+ async function syncOnce(state, target) {
104
+ const stage = await runStage(state, target, { mode: 'hook' });
105
+ if (stage.kind === 'stop') {
106
+ if (stage.note)
107
+ writeNote(state, target.slug, stage.note);
108
+ return;
109
+ }
110
+ const remote = new RepoSyncRemote(target.rag(), stage.bucketId);
111
+ let token = stage.token;
112
+ try {
113
+ const { plan } = stage;
114
+ const work = plan.rewrite.length + plan.partial.length;
115
+ const commit = stage.head;
116
+ if (plan.suspicious) {
117
+ writeNote(state, target.slug, { kind: 'mass_delete', files: plan.goneCount });
118
+ return;
119
+ }
120
+ if (work === 0) {
121
+ await finishSync(state, target, { bucketId: stage.bucketId, token, mode: 'hook' });
122
+ return;
123
+ }
124
+ if (work > maxRewriteFiles()) {
125
+ writeNote(state, target.slug, { kind: 'needs_approval', files: work, ...(commit ? { commit } : {}) });
126
+ return;
127
+ }
128
+ if (!isClean(target.root)) {
129
+ writeNote(state, target.slug, { kind: 'dirty', files: plan.stale.length, ...(commit ? { commit } : {}) });
130
+ return;
131
+ }
132
+ const claude = process.env['GIGARAG_NO_REWRITE'] ? undefined : findClaude(process.env, process.platform);
133
+ if (!claude) {
134
+ writeNote(state, target.slug, { kind: 'no_agent', files: plan.stale.length, ...(commit ? { commit } : {}) });
135
+ return;
136
+ }
137
+ const outcome = await runRewriter(claudeInvocation(claude, process.platform), target.root, join(stateDir(), 'logs', `sync-${target.slug}.log`), {
138
+ timeoutMs: CHILD_TIMEOUT_MS,
139
+ tickMs: TICK_MS,
140
+ onTick: async () => {
141
+ const renewed = await remote.claim(holderFor(state, 'hook'), token);
142
+ if (renewed.granted)
143
+ token = renewed.token;
144
+ },
145
+ env: { ...process.env, GIGARAG_SYNC_WORKER: '1' },
146
+ });
147
+ if (outcome.timedOut || outcome.code !== 0) {
148
+ writeNote(state, target.slug, { kind: 'failed', detail: outcome.timedOut ? 'it ran past 40 minutes and was stopped' : `claude exited with code ${outcome.code ?? 'none'}` });
149
+ return;
150
+ }
151
+ const fin = await finishSync(state, target, { bucketId: stage.bucketId, token, mode: 'hook' });
152
+ if (!fin.advanced)
153
+ writeNote(state, target.slug, { kind: 'failed', detail: `the agent finished with ${fin.remaining} files still to rewrite` });
154
+ }
155
+ finally {
156
+ await remote.release(token);
157
+ state.setMeta(leaseMetaKey(target.slug, 'hook'), '');
158
+ }
159
+ }
@@ -5,6 +5,12 @@ argument-hint: [path]
5
5
 
6
6
  Index the codebase at "$ARGUMENTS" into GigaRAG. If that is empty, use the current directory.
7
7
 
8
+ First, check which workspace this repository uses. Run `gigarag repo` in it. If the output has no `workspace`, run `gigarag workspaces --json`. When that lists more than one workspace, ask me which one this repository belongs to, showing each name and how I reach it, then run `gigarag use <id>` in the repository root. If I pick a workspace where I am a guest, `gigarag use` asks for `--bucket` and lists the buckets I can write to; ask me which, and run it again with `--bucket <slug>`. When it lists only one workspace, use it without asking.
9
+
8
10
  Hand the work to the `gigarag:gigarag-indexer` subagent, so the repository walk happens in its own context and not in this conversation. Run it in the foreground and wait for it to finish, because a background run ends with nothing indexed if this session closes first. Give it the path exactly as I gave it, or the current directory, and tell it to run a full index.
9
11
 
10
- When it finishes, tell me in five lines or fewer: the bucket it used, how many memos it created, updated and deleted, anything it could not index and why, and whether the index is now up to date. Do not paste the file list.
12
+ If `gigarag scan` (which the subagent runs first) refuses because `.gigarag.json` names a workspace that has not been trusted, it exits nonzero and prints why instead of a manifest. Stop there, relay exactly what it printed to me, and do not run `gigarag use` on my behalf: someone should look at that file before it is trusted or replaced. If I placed or reviewed it myself I run `gigarag trust`; if it should point elsewhere I run `gigarag use <workspace>` myself. Only then run `/gigaindex` again.
13
+
14
+ When it finishes, run `gigarag sync end --init` in the directory that was indexed. It saves the index on the server, records the current branch as the one GigaRAG keeps in sync, and installs git hooks that sync after each commit on it.
15
+
16
+ Then tell me in five lines or fewer: the bucket it used, how many memos it created, updated and deleted, anything it could not index and why, whether the index is now up to date, and the last line `gigarag sync end --init` printed. Do not paste the file list.
@@ -2,8 +2,10 @@
2
2
  description: Bring GigaRAG up to date with the files that changed since the last index
3
3
  ---
4
4
 
5
- Run `gigarag scan --summary` in the current directory and read the counts.
5
+ Run `gigarag sync begin` in the current directory. It claims this repository's sync lease, applies the renames and deletions that need no model, and marks the memos of changed files stale.
6
6
 
7
- If nothing is new, changed or gone, say "GigaRAG is up to date with this repository." and stop. Do not start the indexer for nothing.
7
+ If it exits with status 3, tell me what it printed, in its words, and stop. Do not start the indexer.
8
8
 
9
- Otherwise hand the work to the `gigarag:gigarag-indexer` subagent, in the foreground and waiting for it to finish, for the current directory, and tell it this is an incremental sync: only the files the scan reports. When it finishes, tell me in three lines or fewer what changed: memos created, updated and deleted.
9
+ Then run `gigarag scan --summary` and read the counts. If nothing is new, changed or gone, run `gigarag sync end`, say "GigaRAG is up to date with this repository." and stop.
10
+
11
+ Otherwise hand the work to the `gigarag:gigarag-indexer` subagent, in the foreground and waiting for it to finish, for the current directory, and tell it this is an incremental sync: only the files the scan reports. When it finishes, run `gigarag sync end`. Tell me in three lines or fewer what changed: memos created, updated and deleted, and the last line `gigarag sync end` printed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gigarag-claude-code",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "The GigaRAG plugin for Claude Code. Commands, an indexing agent, hooks and an MCP server that carry the gigarag CLI inside them.",
5
5
  "license": "UNLICENSED",
6
6
  "files": [