gigarag-copilot 0.1.0
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/README.md +31 -0
- package/cli/auth/credentials.js +106 -0
- package/cli/auth/oauth.js +203 -0
- package/cli/auth/page.js +68 -0
- package/cli/bin.js +8 -0
- package/cli/cli.js +90 -0
- package/cli/clients/commands.js +160 -0
- package/cli/clients/connect.js +217 -0
- package/cli/clients/inspect.js +74 -0
- package/cli/clients/json.js +135 -0
- package/cli/clients/launcher.js +71 -0
- package/cli/clients/registry.js +40 -0
- package/cli/clients/toml.js +169 -0
- package/cli/clients/tomlarray.js +121 -0
- package/cli/clients/yaml.js +146 -0
- package/cli/clients.json +1226 -0
- package/cli/commands/authHeader.js +22 -0
- package/cli/commands/connect.js +285 -0
- package/cli/commands/indexSync.js +46 -0
- package/cli/commands/login.js +129 -0
- package/cli/commands/mcp.js +22 -0
- package/cli/commands/record.js +72 -0
- package/cli/commands/repo.js +48 -0
- package/cli/commands/scan.js +72 -0
- package/cli/commands/status.js +115 -0
- package/cli/config.js +69 -0
- package/cli/connect.js +8 -0
- package/cli/constants.js +24 -0
- package/cli/hooks.js +151 -0
- package/cli/index.js +3 -0
- package/cli/mcp/bridge.js +123 -0
- package/cli/mcp/client.js +156 -0
- package/cli/mcp/session.js +79 -0
- package/cli/package.json +5 -0
- package/cli/paths.js +34 -0
- package/cli/prompts.generated.js +44 -0
- package/cli/prompts.js +48 -0
- package/cli/scan/chunk.js +43 -0
- package/cli/scan/ignore.js +117 -0
- package/cli/scan/repo.js +99 -0
- package/cli/scan/scan.js +262 -0
- package/cli/scan.js +5 -0
- package/cli/sdk.js +130 -0
- package/cli/secrets.js +192 -0
- package/cli/secureUrl.js +18 -0
- package/cli/state.js +210 -0
- package/cli/ui.js +66 -0
- package/mcp.json +8 -0
- package/package.json +20 -0
- package/plugin.json +10 -0
- package/scripts/run.mjs +64 -0
- package/skills/gigadocs/SKILL.md +21 -0
- package/skills/gigaindex/SKILL.md +74 -0
- package/skills/gigarecall/SKILL.md +17 -0
- package/skills/gigasave/SKILL.md +26 -0
- package/skills/gigasync/SKILL.md +76 -0
package/cli/state.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { chmodSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { stateDir } from './paths.js';
|
|
5
|
+
/**
|
|
6
|
+
* node:sqlite prints an ExperimentalWarning the first time it loads. A hook's
|
|
7
|
+
* stderr lands in the middle of somebody's terminal session, so the warning is
|
|
8
|
+
* swallowed for the one require and the original handler put back.
|
|
9
|
+
*/
|
|
10
|
+
function loadSqlite() {
|
|
11
|
+
const original = process.emitWarning;
|
|
12
|
+
process.emitWarning = ((warning, ...rest) => {
|
|
13
|
+
const type = typeof rest[0] === 'string' ? rest[0] : rest[0]?.type;
|
|
14
|
+
if (type === 'ExperimentalWarning' || (warning instanceof Error && warning.name === 'ExperimentalWarning'))
|
|
15
|
+
return;
|
|
16
|
+
return original.call(process, warning, ...rest);
|
|
17
|
+
});
|
|
18
|
+
try {
|
|
19
|
+
return createRequire(import.meta.url)('node:sqlite');
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
process.emitWarning = original;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const SCHEMA = `
|
|
26
|
+
CREATE TABLE IF NOT EXISTS repos (
|
|
27
|
+
slug TEXT PRIMARY KEY, root TEXT NOT NULL, indexed_at INTEGER
|
|
28
|
+
);
|
|
29
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
30
|
+
repo TEXT NOT NULL, path TEXT NOT NULL, hash TEXT NOT NULL,
|
|
31
|
+
dirty INTEGER NOT NULL DEFAULT 0, indexed_at INTEGER,
|
|
32
|
+
PRIMARY KEY (repo, path)
|
|
33
|
+
);
|
|
34
|
+
CREATE TABLE IF NOT EXISTS links (
|
|
35
|
+
repo TEXT NOT NULL, path TEXT NOT NULL, node_id TEXT NOT NULL,
|
|
36
|
+
PRIMARY KEY (repo, path, node_id)
|
|
37
|
+
);
|
|
38
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
39
|
+
node_id TEXT PRIMARY KEY, ref TEXT, title TEXT NOT NULL, type TEXT,
|
|
40
|
+
tokens INTEGER, touched_at INTEGER NOT NULL
|
|
41
|
+
);
|
|
42
|
+
CREATE INDEX IF NOT EXISTS nodes_touched ON nodes (touched_at DESC);
|
|
43
|
+
CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
44
|
+
`;
|
|
45
|
+
export class State {
|
|
46
|
+
db;
|
|
47
|
+
cache = new Map();
|
|
48
|
+
constructor(path = join(stateDir(), 'state.db')) {
|
|
49
|
+
// Private, like the key file beside it: memo titles and repository paths are not for other users of the machine.
|
|
50
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
51
|
+
const { DatabaseSync } = loadSqlite();
|
|
52
|
+
this.db = new DatabaseSync(path);
|
|
53
|
+
// Several Claude Code sessions write to this file at once, each from its own hook process.
|
|
54
|
+
this.db.exec('PRAGMA busy_timeout = 2000; PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL;');
|
|
55
|
+
this.db.exec(SCHEMA);
|
|
56
|
+
try {
|
|
57
|
+
chmodSync(path, 0o600);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
/* Windows has no chmod that means anything */
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
stmt(sql) {
|
|
64
|
+
let s = this.cache.get(sql);
|
|
65
|
+
if (!s) {
|
|
66
|
+
s = this.db.prepare(sql);
|
|
67
|
+
this.cache.set(sql, s);
|
|
68
|
+
}
|
|
69
|
+
return s;
|
|
70
|
+
}
|
|
71
|
+
close() {
|
|
72
|
+
this.db.close();
|
|
73
|
+
}
|
|
74
|
+
transaction(fn) {
|
|
75
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
76
|
+
try {
|
|
77
|
+
const out = fn();
|
|
78
|
+
this.db.exec('COMMIT');
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
this.db.exec('ROLLBACK');
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// --- meta ---------------------------------------------------------------
|
|
87
|
+
getMeta(key) {
|
|
88
|
+
return this.stmt('SELECT value FROM meta WHERE key = ?').get(key)?.value;
|
|
89
|
+
}
|
|
90
|
+
setMeta(key, value) {
|
|
91
|
+
this.stmt('INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value').run(key, value);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Takes a lock that lapses after `ttlMs`, in one transaction, so two workers starting together
|
|
95
|
+
* cannot both see it free. Returns whether this caller now holds it.
|
|
96
|
+
*/
|
|
97
|
+
tryAcquire(key, ttlMs, now = Date.now()) {
|
|
98
|
+
return this.transaction(() => {
|
|
99
|
+
if (now - Number(this.getMeta(key) ?? 0) < ttlMs)
|
|
100
|
+
return false;
|
|
101
|
+
this.setMeta(key, String(now));
|
|
102
|
+
return true;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
// --- repos and files ------------------------------------------------------
|
|
106
|
+
upsertRepo(slug, root, indexedAt) {
|
|
107
|
+
this.stmt(`INSERT INTO repos (slug, root, indexed_at) VALUES (?, ?, ?)
|
|
108
|
+
ON CONFLICT(slug) DO UPDATE SET root = excluded.root, indexed_at = COALESCE(excluded.indexed_at, indexed_at)`).run(slug, root, indexedAt ?? null);
|
|
109
|
+
}
|
|
110
|
+
listRepos() {
|
|
111
|
+
return this.stmt('SELECT slug, root, indexed_at FROM repos ORDER BY slug').all();
|
|
112
|
+
}
|
|
113
|
+
hasRepo(slug) {
|
|
114
|
+
return this.stmt('SELECT 1 AS x FROM repos WHERE slug = ?').get(slug) !== undefined;
|
|
115
|
+
}
|
|
116
|
+
repoBySlug(slug) {
|
|
117
|
+
return this.stmt('SELECT slug, root, indexed_at FROM repos WHERE slug = ?').get(slug);
|
|
118
|
+
}
|
|
119
|
+
files(repo) {
|
|
120
|
+
const rows = this.stmt('SELECT path, hash, dirty FROM files WHERE repo = ?').all(repo);
|
|
121
|
+
return new Map(rows.map(r => [r.path, r]));
|
|
122
|
+
}
|
|
123
|
+
nodesOf(repo, path) {
|
|
124
|
+
return this.stmt('SELECT node_id FROM links WHERE repo = ? AND path = ?').all(repo, path).map(r => r.node_id);
|
|
125
|
+
}
|
|
126
|
+
/** Nodes that only these paths point at, so deleting the files means deleting them. */
|
|
127
|
+
orphansIfRemoved(repo, paths) {
|
|
128
|
+
const removing = new Set(paths);
|
|
129
|
+
const out = new Map();
|
|
130
|
+
for (const path of paths) {
|
|
131
|
+
const orphaned = [];
|
|
132
|
+
for (const id of this.nodesOf(repo, path)) {
|
|
133
|
+
const holders = this.stmt('SELECT repo, path FROM links WHERE node_id = ?').all(id);
|
|
134
|
+
const remaining = holders.filter(h => !(h.repo === repo && removing.has(h.path)));
|
|
135
|
+
if (remaining.length === 0)
|
|
136
|
+
orphaned.push(id);
|
|
137
|
+
}
|
|
138
|
+
out.set(path, orphaned);
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
/** Records that `path` was indexed at `hash` and now lives in `nodeIds`. */
|
|
143
|
+
recordFile(repo, path, hash, nodeIds, now = Date.now()) {
|
|
144
|
+
this.transaction(() => {
|
|
145
|
+
this.stmt(`INSERT INTO files (repo, path, hash, dirty, indexed_at) VALUES (?, ?, ?, 0, ?)
|
|
146
|
+
ON CONFLICT(repo, path) DO UPDATE SET hash = excluded.hash, dirty = 0, indexed_at = excluded.indexed_at`).run(repo, path, hash, now);
|
|
147
|
+
for (const id of nodeIds) {
|
|
148
|
+
this.stmt('INSERT OR IGNORE INTO links (repo, path, node_id) VALUES (?, ?, ?)').run(repo, path, id);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
forgetFile(repo, path) {
|
|
153
|
+
this.transaction(() => {
|
|
154
|
+
this.stmt('DELETE FROM files WHERE repo = ? AND path = ?').run(repo, path);
|
|
155
|
+
this.stmt('DELETE FROM links WHERE repo = ? AND path = ?').run(repo, path);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
markDirty(repo, path) {
|
|
159
|
+
const res = this.stmt('UPDATE files SET dirty = 1 WHERE repo = ? AND path = ?').run(repo, path);
|
|
160
|
+
return Number(res.changes) > 0;
|
|
161
|
+
}
|
|
162
|
+
/** A file saved without changing has nothing to re-index, so its flag goes. */
|
|
163
|
+
clearDirty(repo, path) {
|
|
164
|
+
this.stmt('UPDATE files SET dirty = 0 WHERE repo = ? AND path = ? AND dirty = 1').run(repo, path);
|
|
165
|
+
}
|
|
166
|
+
dirtyCount(repo) {
|
|
167
|
+
return this.stmt('SELECT COUNT(*) AS n FROM files WHERE repo = ? AND dirty = 1').get(repo).n;
|
|
168
|
+
}
|
|
169
|
+
dirtyFiles(repo) {
|
|
170
|
+
return this.stmt('SELECT path FROM files WHERE repo = ? AND dirty = 1 ORDER BY path').all(repo).map(r => r.path);
|
|
171
|
+
}
|
|
172
|
+
// --- the session index ------------------------------------------------------
|
|
173
|
+
upsertNode(row) {
|
|
174
|
+
this.stmt(`INSERT INTO nodes (node_id, ref, title, type, tokens, touched_at) VALUES (?, ?, ?, ?, ?, ?)
|
|
175
|
+
ON CONFLICT(node_id) DO UPDATE SET
|
|
176
|
+
ref = COALESCE(excluded.ref, ref), title = excluded.title, type = COALESCE(excluded.type, type),
|
|
177
|
+
tokens = COALESCE(excluded.tokens, tokens), touched_at = excluded.touched_at`).run(row.node_id, row.ref ?? null, row.title, row.type ?? null, row.tokens ?? null, row.touched_at ?? Date.now());
|
|
178
|
+
}
|
|
179
|
+
deleteNode(nodeId) {
|
|
180
|
+
this.transaction(() => {
|
|
181
|
+
this.stmt('DELETE FROM nodes WHERE node_id = ?').run(nodeId);
|
|
182
|
+
this.stmt('DELETE FROM links WHERE node_id = ?').run(nodeId);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
/** Replaces the synced set: rows not in `keep` are dropped, so deletions elsewhere disappear here. */
|
|
186
|
+
replaceNodes(rows) {
|
|
187
|
+
this.transaction(() => {
|
|
188
|
+
const known = new Map(this.stmt('SELECT node_id, tokens FROM nodes').all().map(r => [r.node_id, r.tokens]));
|
|
189
|
+
this.db.exec('DELETE FROM nodes');
|
|
190
|
+
for (const row of rows) {
|
|
191
|
+
this.stmt('INSERT INTO nodes (node_id, ref, title, type, tokens, touched_at) VALUES (?, ?, ?, ?, ?, ?)').run(row.node_id, row.ref, row.title, row.type, row.tokens ?? known.get(row.node_id) ?? null, row.touched_at);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
recentNodes(limit) {
|
|
196
|
+
return this.stmt('SELECT node_id, ref, title, type, tokens, touched_at FROM nodes ORDER BY touched_at DESC LIMIT ?').all(limit);
|
|
197
|
+
}
|
|
198
|
+
nodeCount() {
|
|
199
|
+
return this.stmt('SELECT COUNT(*) AS n FROM nodes').get().n;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/** Opens the state, or returns undefined when it cannot be opened, so a hook never fails a session. */
|
|
203
|
+
export function tryOpenState(path) {
|
|
204
|
+
try {
|
|
205
|
+
return new State(path);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
}
|
package/cli/ui.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { stdin, stdout } from 'node:process';
|
|
2
|
+
export const out = (line = '') => stdout.write(`${line}\n`);
|
|
3
|
+
export const err = (line = '') => process.stderr.write(`${line}\n`);
|
|
4
|
+
/** Pads columns so a list reads as a table without box drawing. */
|
|
5
|
+
export function table(rows, gap = 2) {
|
|
6
|
+
const widths = [];
|
|
7
|
+
for (const row of rows)
|
|
8
|
+
row.forEach((cell, i) => (widths[i] = Math.max(widths[i] ?? 0, cell.length)));
|
|
9
|
+
return rows.map(row => row
|
|
10
|
+
.map((cell, i) => (i === row.length - 1 ? cell : cell.padEnd(widths[i] + gap)))
|
|
11
|
+
.join('')
|
|
12
|
+
.trimEnd());
|
|
13
|
+
}
|
|
14
|
+
/** Reads a line from the terminal without echoing it, for a key that should not land in scrollback. */
|
|
15
|
+
export function promptHidden(question) {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
if (!stdin.isTTY) {
|
|
18
|
+
reject(new Error('No terminal to prompt on.'));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
stdout.write(question);
|
|
22
|
+
let value = '';
|
|
23
|
+
stdin.setRawMode(true);
|
|
24
|
+
stdin.resume();
|
|
25
|
+
stdin.setEncoding('utf8');
|
|
26
|
+
const done = (fn) => {
|
|
27
|
+
stdin.setRawMode(false);
|
|
28
|
+
stdin.pause();
|
|
29
|
+
stdin.off('data', onData);
|
|
30
|
+
stdout.write('\n');
|
|
31
|
+
fn();
|
|
32
|
+
};
|
|
33
|
+
const onData = (chunk) => {
|
|
34
|
+
for (const ch of chunk) {
|
|
35
|
+
if (ch === '\r' || ch === '\n')
|
|
36
|
+
return done(() => resolve(value));
|
|
37
|
+
if (ch === '\u0003')
|
|
38
|
+
return done(() => reject(new Error('Cancelled.')));
|
|
39
|
+
if (ch === '\u007f' || ch === '\b')
|
|
40
|
+
value = value.slice(0, -1);
|
|
41
|
+
else if (ch >= ' ')
|
|
42
|
+
value += ch;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
stdin.on('data', onData);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/** First line of piped input, for `echo $KEY | gigarag login`. */
|
|
49
|
+
export async function readStdinLine() {
|
|
50
|
+
const chunks = [];
|
|
51
|
+
for await (const chunk of stdin)
|
|
52
|
+
chunks.push(chunk);
|
|
53
|
+
return Buffer.concat(chunks).toString('utf8').split(/\r?\n/)[0]?.trim() ?? '';
|
|
54
|
+
}
|
|
55
|
+
export async function readStdinAll() {
|
|
56
|
+
const chunks = [];
|
|
57
|
+
for await (const chunk of stdin)
|
|
58
|
+
chunks.push(chunk);
|
|
59
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
60
|
+
}
|
|
61
|
+
export class UsageError extends Error {
|
|
62
|
+
constructor(message) {
|
|
63
|
+
super(message);
|
|
64
|
+
this.name = 'UsageError';
|
|
65
|
+
}
|
|
66
|
+
}
|
package/mcp.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gigarag-copilot",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The GigaRAG plugin for VS Code and Copilot CLI. Skills and an MCP server that carry the gigarag CLI inside them.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"files": [
|
|
7
|
+
"plugin.json",
|
|
8
|
+
"skills",
|
|
9
|
+
"scripts",
|
|
10
|
+
"cli",
|
|
11
|
+
"mcp.json",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/gigarag/gigarag-cli.git",
|
|
17
|
+
"directory": "plugins/copilot"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://gigarag.com"
|
|
20
|
+
}
|
package/plugin.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
|
+
"name": "gigarag",
|
|
4
|
+
"description": "Your GigaRAG knowledge base inside VS Code and Copilot CLI: index a codebase, save decisions, recall them later.",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"author": { "name": "GigaRAG", "url": "https://gigarag.com" },
|
|
7
|
+
"homepage": "https://gigarag.com/connect/copilot-plugins",
|
|
8
|
+
"license": "UNLICENSED",
|
|
9
|
+
"keywords": ["memory", "rag", "knowledge-base", "mcp"]
|
|
10
|
+
}
|
package/scripts/run.mjs
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Finds the gigarag CLI and runs it, so every hook, command and MCP entry in this
|
|
2
|
+
// plugin calls one thing. Lookup order:
|
|
3
|
+
// 1. GIGARAG_CLI, a path to bin.js, for development
|
|
4
|
+
// 2. ../cli/bin.js, the build vendored into this package at release time
|
|
5
|
+
// 3. a gigarag on PATH, such as npm i -g gigarag or the standalone installer
|
|
6
|
+
// 4. npx, which downloads the version this plugin was released with
|
|
7
|
+
// The CLI is vendored because Claude Code does not run install scripts and npm never
|
|
8
|
+
// publishes a lockfile, so a declared dependency would not be there on first run.
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
11
|
+
import { dirname, join } from 'node:path';
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
13
|
+
|
|
14
|
+
const args = process.argv.slice(2);
|
|
15
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const isHook = args[0] === 'hook';
|
|
17
|
+
const windows = process.platform === 'win32';
|
|
18
|
+
|
|
19
|
+
const local = [process.env.GIGARAG_CLI, join(here, '..', 'cli', 'bin.js')].find(p => p && existsSync(p));
|
|
20
|
+
|
|
21
|
+
/** The version this plugin shipped with, so npx runs the CLI it was tested against and not whatever is newest. */
|
|
22
|
+
const pinned = () => {
|
|
23
|
+
try {
|
|
24
|
+
const { version } = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8'));
|
|
25
|
+
return /^\d+\.\d+\.\d+(-[\w.]+)?$/.test(version) ? `gigarag@${version}` : 'gigarag';
|
|
26
|
+
} catch {
|
|
27
|
+
return 'gigarag';
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** True when the command exists. A probe, because a shell reports a missing command as an ordinary exit code. */
|
|
32
|
+
const exists = (cmd, probe) => {
|
|
33
|
+
const r = spawnSync(cmd, probe, { stdio: 'ignore', shell: windows });
|
|
34
|
+
return !r.error && r.status === 0;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* On Windows gigarag and npx are .cmd scripts, which Node will only start through cmd.exe. What a
|
|
39
|
+
* user typed after a slash command ends up in these arguments, so anything cmd.exe would read as
|
|
40
|
+
* syntax is refused rather than passed on.
|
|
41
|
+
*/
|
|
42
|
+
const unsafeForCmd = list => windows && list.some(a => /[&|<>^%"\r\n`]/.test(a));
|
|
43
|
+
|
|
44
|
+
if (local) {
|
|
45
|
+
// In this process: no second Node start, which matters inside a 100ms hook budget.
|
|
46
|
+
await import(pathToFileURL(local).href);
|
|
47
|
+
} else {
|
|
48
|
+
const attempts = [
|
|
49
|
+
['gigarag', ['--version'], 'gigarag', args],
|
|
50
|
+
['npx', ['--version'], 'npx', ['-y', pinned(), ...args]],
|
|
51
|
+
];
|
|
52
|
+
const found = attempts.find(([probeCmd, probeArgs]) => exists(probeCmd, probeArgs));
|
|
53
|
+
if (found && unsafeForCmd(found[3])) {
|
|
54
|
+
if (!isHook) process.stderr.write('gigarag: an argument holds a character that cmd.exe would treat as syntax, so nothing was run.\n');
|
|
55
|
+
process.exitCode = isHook ? 0 : 1;
|
|
56
|
+
} else if (found) {
|
|
57
|
+
const r = spawnSync(found[2], found[3], { stdio: 'inherit', shell: windows });
|
|
58
|
+
// A hook must never break the session it runs in.
|
|
59
|
+
process.exitCode = isHook ? 0 : (r.status ?? 1);
|
|
60
|
+
} else {
|
|
61
|
+
if (!isHook) process.stderr.write('gigarag is not installed. Run: npm install -g gigarag\n');
|
|
62
|
+
process.exitCode = isHook ? 0 : 1;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gigadocs
|
|
3
|
+
description: Ingest external documentation into GigaRAG so it can be searched later
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Run the CLI as `gigarag`. If the shell says it is not found, use `npx -y gigarag` in its place, with the same arguments.
|
|
7
|
+
|
|
8
|
+
Ingest the documentation at the text the user typed after the skill name into GigaRAG.
|
|
9
|
+
|
|
10
|
+
If that is empty or is not a URL, ask me for one and stop.
|
|
11
|
+
|
|
12
|
+
The bucket is named for the site: the hostname with dots turned into hyphens and any leading `www` dropped, so docs.stripe.com becomes `docs-stripe-com`. Find it with `list_buckets`, and create it if it is missing, with the site name as the title.
|
|
13
|
+
|
|
14
|
+
Then:
|
|
15
|
+
1. Fetch the page. Follow links that stay on the same host and sit under the same path prefix as the starting URL, up to two levels deep and at most 30 pages. Skip anchors, downloads, login pages and anything that repeats a page you already have. Say so if you hit the 30 page limit.
|
|
16
|
+
2. For each page, first call `search_nodes` for its URL. If a memo already carries that URL, update it in place with `update_node` instead of creating a duplicate, so running this twice does not double the bucket.
|
|
17
|
+
3. Otherwise `create_node`. The title is the page's own title. The summary is one or two sentences on what the page covers. The body opens with the line `Source: <url>, retrieved <today's date>`, then the page's content as clean markdown: keep headings, code blocks and tables, drop navigation, footers, cookie banners and repeated menus.
|
|
18
|
+
4. A memo body holds at most 10,000 characters. For a longer page, split at a heading into several memos titled "Page title, part 2" and so on, and link each part to the next in its text as `[Page title, part 2](N:12)`.
|
|
19
|
+
5. Set `node_type` to `doc` and `actor` to `agent:claude-code`.
|
|
20
|
+
|
|
21
|
+
When you finish, tell me the bucket, how many pages you created and updated, how many you skipped and why, and one example query I could try with `/gigarecall`.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gigaindex
|
|
3
|
+
description: Index a codebase into GigaRAG, one bucket per repository
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Run the CLI as `gigarag`. If the shell says it is not found, use `npx -y gigarag` in its place, with the same arguments.
|
|
7
|
+
|
|
8
|
+
This needs a shell tool, because it runs the `gigarag` command. If you have no way to run shell commands, say so in one sentence and stop. Do not guess at what the repository contains.
|
|
9
|
+
|
|
10
|
+
Index a codebase into GigaRAG. The path to index is: "the text the user typed after the skill name". If that is empty, use the current directory.
|
|
11
|
+
|
|
12
|
+
Do the work yourself, in this conversation, following the procedure below. When you finish, tell the user in five lines or fewer: the bucket, how many memos you created, updated and deleted, anything you could not index and why, and whether the index is up to date. Do not paste the file list.
|
|
13
|
+
|
|
14
|
+
## Procedure
|
|
15
|
+
|
|
16
|
+
The work is split on purpose. The `gigarag` command does the mechanical half: it walks the tree, respects gitignore, hashes contents, plans chunk boundaries and works out what changed. You do the judgement half: what a module is for, what is worth summarising, and what links to what. Never walk the filesystem yourself with ls or find. The manifest names every file you need.
|
|
17
|
+
|
|
18
|
+
## 1. Scan
|
|
19
|
+
|
|
20
|
+
Run `gigarag scan <path>` with the path you were given, or the current directory. It prints a JSON manifest. Read these fields:
|
|
21
|
+
|
|
22
|
+
- `repo.slug` is the bucket. `repo.root` is the repository root.
|
|
23
|
+
- `files` lists new and changed files, each with `path`, `hash`, `status`, `lines`, `language`, `nodeIds` (memos already made from it) and sometimes `chunks` (line ranges, when the file is too long for one memo).
|
|
24
|
+
- `removed` lists files that are gone, each with `nodeIds` and `deleteNodeIds`.
|
|
25
|
+
- `truncated: true` means the walk hit its limit. Tell the user in your report and carry on with what you have.
|
|
26
|
+
|
|
27
|
+
If `files` and `removed` are both empty, report "Up to date" and stop.
|
|
28
|
+
|
|
29
|
+
## 2. Find or create the bucket and threads
|
|
30
|
+
|
|
31
|
+
Call `list_buckets` with `q` set to the slug. Use the bucket whose slug matches exactly, or `create_bucket` with that slug, the repository name as the title, and a one line description. Slugs are lowercase letters, digits and hyphens, at most 64 characters.
|
|
32
|
+
|
|
33
|
+
Group memos into threads by the top level area of the repository: `src`, `docs`, `tests`, `config`. Look them up with `list_threads` for the bucket and create the missing ones. Keep the number of threads under about ten.
|
|
34
|
+
|
|
35
|
+
## 3. Write the memos
|
|
36
|
+
|
|
37
|
+
Work in batches of about ten files. For each file:
|
|
38
|
+
|
|
39
|
+
- Read it with the Read tool. When it has `chunks`, read one range at a time using `offset` and `limit`.
|
|
40
|
+
- Write a memo that explains the file to somebody who has not read it. Cover what it is for, its public surface (exported names and signatures, not their bodies), how it connects to the rest of the system, and any non-obvious constraint. Aim for 500 to 2,500 characters. Do not paste the source. A memo that only restates the code is worse than none.
|
|
41
|
+
- Very small files that only make sense together, such as a folder of three tiny helpers, may share one memo. Give each of those files the same node when you record them.
|
|
42
|
+
- The title says what the module is, for example "Session token refresh". The summary is one or two sentences. `node_type` is `code` for source, `doc` for markdown and prose, `config` for configuration. The slug comes from the path, for example `src-auth-session-ts`. Set `actor` to `agent:gigarag-indexer`.
|
|
43
|
+
- A body holds at most 10,000 characters. For a long file with `chunks`, write one memo per chunk and a short parent memo that links to them.
|
|
44
|
+
- If `nodeIds` is not empty, the file was indexed before. Call `update_node` on that node instead of creating a second one, and keep the `[Title](N:12)` links already in its text unless the thing they point at is gone.
|
|
45
|
+
- Where this memo depends on another module, link it in the text as `[Module name](N:12)`. Get the ref from `find_nodes` with part of the title, or from a memo you have already written this run. Never write a UUID as a link target. After each write, look at `unresolved_links` in the reply and fix or remove any ref that did not resolve.
|
|
46
|
+
|
|
47
|
+
## 4. Record progress after every batch
|
|
48
|
+
|
|
49
|
+
After each batch, pipe the result to `gigarag record` so a later run updates in place and an interrupted run can resume. Send JSON on stdin:
|
|
50
|
+
|
|
51
|
+
{"repo":"<slug>","files":[{"path":"src/auth.ts","hash":"<hash from the manifest>","nodes":[{"id":"<node id>","ref":"N:12","title":"Session token refresh","type":"code","tokens":600}]}]}
|
|
52
|
+
|
|
53
|
+
`tokens` is your rough estimate of what fetching the memo costs, at about four characters a token. Do this per batch, not once at the end.
|
|
54
|
+
|
|
55
|
+
## 5. Remove what is gone
|
|
56
|
+
|
|
57
|
+
For each entry in `removed`, call `delete_node` for every id in its `deleteNodeIds`. For memos that other files still feed, use `update_node` to take the removed file out of the text. Then record it:
|
|
58
|
+
|
|
59
|
+
{"repo":"<slug>","removed":[{"path":"src/old.ts","deletedNodeIds":["<node id>"]}]}
|
|
60
|
+
|
|
61
|
+
Without this half the index only ever grows and drifts away from the repository while still looking correct.
|
|
62
|
+
|
|
63
|
+
## Rate limits
|
|
64
|
+
|
|
65
|
+
If a tool answers that you are rate limited, wait and continue. Do not start over. Files you have already recorded are skipped by the next `gigarag scan`, so an interrupted index resumes where it stopped.
|
|
66
|
+
|
|
67
|
+
## Report
|
|
68
|
+
|
|
69
|
+
End with this, and nothing longer:
|
|
70
|
+
|
|
71
|
+
- Bucket: the slug
|
|
72
|
+
- Memos: created N, updated N, deleted N
|
|
73
|
+
- Not indexed: what and why, or "none"
|
|
74
|
+
- Status: up to date, or what is left
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gigarecall
|
|
3
|
+
description: Search GigaRAG and load what it knows into this conversation
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Run the CLI as `gigarag`. If the shell says it is not found, use `npx -y gigarag` in its place, with the same arguments.
|
|
7
|
+
|
|
8
|
+
Recall what GigaRAG knows about: the text the user typed after the skill name
|
|
9
|
+
|
|
10
|
+
If that is empty, ask me what to recall and stop.
|
|
11
|
+
|
|
12
|
+
1. Call `search_nodes` with my question in plain words, limit 8. It combines exact-term and meaning-based search, so a paraphrase works as well as an identifier.
|
|
13
|
+
2. Read the two to four most relevant hits in full with `fetch_node`. Search returns only a snippet, so do not answer from snippets alone.
|
|
14
|
+
3. If a memo links to another that looks necessary to answer, read that one too with `fetch_node`, or use `neighbors`. Stop after about six memos in total.
|
|
15
|
+
4. Give me what you found: the facts that answer the question, each with the ref of the memo it came from, like N:12. Say which memos disagree, if any do, and which is newer. If a memo is marked superseded, say so and prefer what replaced it.
|
|
16
|
+
|
|
17
|
+
If nothing relevant came back, say that plainly and suggest one different phrasing to try. Do not fill the gap from general knowledge, and do not save anything.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gigasave
|
|
3
|
+
description: Save this session's decisions and discoveries to GigaRAG as memos
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Run the CLI as `gigarag`. If the shell says it is not found, use `npx -y gigarag` in its place, with the same arguments.
|
|
7
|
+
|
|
8
|
+
Save what this session decided or learned into GigaRAG, so the next session, on any machine or in any client, can find it. If I gave a focus ("the text the user typed after the skill name"), save only that. Otherwise go through the whole conversation.
|
|
9
|
+
|
|
10
|
+
Find the bucket first: run `gigarag repo --slug` and use that slug. If no bucket has it yet, create one with that slug.
|
|
11
|
+
|
|
12
|
+
What is worth a memo:
|
|
13
|
+
- A decision, with why it was made and what was rejected. "We chose X over Y because Z" is the best kind.
|
|
14
|
+
- A discovery that took effort: why something behaves as it does, a constraint nobody wrote down, a bug's real cause.
|
|
15
|
+
- A convention agreed for this codebase.
|
|
16
|
+
- An open question somebody still has to answer.
|
|
17
|
+
|
|
18
|
+
What is not: chatter, status updates, anything already obvious from the code or the git log, and anything secret. Never save keys, tokens, passwords or personal data, even if they appeared in the conversation.
|
|
19
|
+
|
|
20
|
+
For each one:
|
|
21
|
+
1. Call `search_nodes` with the topic first. If a memo already covers it, update that memo with `update_node`, keeping the links already in its text, instead of creating a second one.
|
|
22
|
+
2. Otherwise `create_node` in a thread called `decisions` in that bucket (create the thread if it is missing). Use the type `decision` for decisions and `note` for everything else. Set `actor` to `agent:claude-code`.
|
|
23
|
+
3. The title states the point ("Use cursor pagination for /events"), not the topic ("Pagination"). The summary is one or two sentences. The body has the context, the decision, why, and what was rejected.
|
|
24
|
+
4. Where a memo relates to another, link it in the text as `[Title](N:12)`, using the ref from `find_nodes`. Never write a UUID as a link target.
|
|
25
|
+
|
|
26
|
+
At the end, list what you saved as one line each: the ref, the title, and whether it was created or updated. If there was nothing worth saving, say so and save nothing.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gigasync
|
|
3
|
+
description: Bring GigaRAG up to date with the files that changed since the last index
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Run the CLI as `gigarag`. If the shell says it is not found, use `npx -y gigarag` in its place, with the same arguments.
|
|
7
|
+
|
|
8
|
+
This needs a shell tool, because it runs the `gigarag` command. If you have no way to run shell commands, say so in one sentence and stop. Do not guess at what the repository contains.
|
|
9
|
+
|
|
10
|
+
Run `gigarag scan --summary` in the current directory and read the counts.
|
|
11
|
+
|
|
12
|
+
If nothing is new, changed or gone, say "GigaRAG is up to date with this repository." and stop.
|
|
13
|
+
|
|
14
|
+
Otherwise do an incremental sync yourself, in this conversation, following the procedure below and touching only the files the scan reports. When you finish, tell the user in three lines or fewer what changed: memos created, updated and deleted.
|
|
15
|
+
|
|
16
|
+
## Procedure
|
|
17
|
+
|
|
18
|
+
The work is split on purpose. The `gigarag` command does the mechanical half: it walks the tree, respects gitignore, hashes contents, plans chunk boundaries and works out what changed. You do the judgement half: what a module is for, what is worth summarising, and what links to what. Never walk the filesystem yourself with ls or find. The manifest names every file you need.
|
|
19
|
+
|
|
20
|
+
## 1. Scan
|
|
21
|
+
|
|
22
|
+
Run `gigarag scan <path>` with the path you were given, or the current directory. It prints a JSON manifest. Read these fields:
|
|
23
|
+
|
|
24
|
+
- `repo.slug` is the bucket. `repo.root` is the repository root.
|
|
25
|
+
- `files` lists new and changed files, each with `path`, `hash`, `status`, `lines`, `language`, `nodeIds` (memos already made from it) and sometimes `chunks` (line ranges, when the file is too long for one memo).
|
|
26
|
+
- `removed` lists files that are gone, each with `nodeIds` and `deleteNodeIds`.
|
|
27
|
+
- `truncated: true` means the walk hit its limit. Tell the user in your report and carry on with what you have.
|
|
28
|
+
|
|
29
|
+
If `files` and `removed` are both empty, report "Up to date" and stop.
|
|
30
|
+
|
|
31
|
+
## 2. Find or create the bucket and threads
|
|
32
|
+
|
|
33
|
+
Call `list_buckets` with `q` set to the slug. Use the bucket whose slug matches exactly, or `create_bucket` with that slug, the repository name as the title, and a one line description. Slugs are lowercase letters, digits and hyphens, at most 64 characters.
|
|
34
|
+
|
|
35
|
+
Group memos into threads by the top level area of the repository: `src`, `docs`, `tests`, `config`. Look them up with `list_threads` for the bucket and create the missing ones. Keep the number of threads under about ten.
|
|
36
|
+
|
|
37
|
+
## 3. Write the memos
|
|
38
|
+
|
|
39
|
+
Work in batches of about ten files. For each file:
|
|
40
|
+
|
|
41
|
+
- Read it with the Read tool. When it has `chunks`, read one range at a time using `offset` and `limit`.
|
|
42
|
+
- Write a memo that explains the file to somebody who has not read it. Cover what it is for, its public surface (exported names and signatures, not their bodies), how it connects to the rest of the system, and any non-obvious constraint. Aim for 500 to 2,500 characters. Do not paste the source. A memo that only restates the code is worse than none.
|
|
43
|
+
- Very small files that only make sense together, such as a folder of three tiny helpers, may share one memo. Give each of those files the same node when you record them.
|
|
44
|
+
- The title says what the module is, for example "Session token refresh". The summary is one or two sentences. `node_type` is `code` for source, `doc` for markdown and prose, `config` for configuration. The slug comes from the path, for example `src-auth-session-ts`. Set `actor` to `agent:gigarag-indexer`.
|
|
45
|
+
- A body holds at most 10,000 characters. For a long file with `chunks`, write one memo per chunk and a short parent memo that links to them.
|
|
46
|
+
- If `nodeIds` is not empty, the file was indexed before. Call `update_node` on that node instead of creating a second one, and keep the `[Title](N:12)` links already in its text unless the thing they point at is gone.
|
|
47
|
+
- Where this memo depends on another module, link it in the text as `[Module name](N:12)`. Get the ref from `find_nodes` with part of the title, or from a memo you have already written this run. Never write a UUID as a link target. After each write, look at `unresolved_links` in the reply and fix or remove any ref that did not resolve.
|
|
48
|
+
|
|
49
|
+
## 4. Record progress after every batch
|
|
50
|
+
|
|
51
|
+
After each batch, pipe the result to `gigarag record` so a later run updates in place and an interrupted run can resume. Send JSON on stdin:
|
|
52
|
+
|
|
53
|
+
{"repo":"<slug>","files":[{"path":"src/auth.ts","hash":"<hash from the manifest>","nodes":[{"id":"<node id>","ref":"N:12","title":"Session token refresh","type":"code","tokens":600}]}]}
|
|
54
|
+
|
|
55
|
+
`tokens` is your rough estimate of what fetching the memo costs, at about four characters a token. Do this per batch, not once at the end.
|
|
56
|
+
|
|
57
|
+
## 5. Remove what is gone
|
|
58
|
+
|
|
59
|
+
For each entry in `removed`, call `delete_node` for every id in its `deleteNodeIds`. For memos that other files still feed, use `update_node` to take the removed file out of the text. Then record it:
|
|
60
|
+
|
|
61
|
+
{"repo":"<slug>","removed":[{"path":"src/old.ts","deletedNodeIds":["<node id>"]}]}
|
|
62
|
+
|
|
63
|
+
Without this half the index only ever grows and drifts away from the repository while still looking correct.
|
|
64
|
+
|
|
65
|
+
## Rate limits
|
|
66
|
+
|
|
67
|
+
If a tool answers that you are rate limited, wait and continue. Do not start over. Files you have already recorded are skipped by the next `gigarag scan`, so an interrupted index resumes where it stopped.
|
|
68
|
+
|
|
69
|
+
## Report
|
|
70
|
+
|
|
71
|
+
End with this, and nothing longer:
|
|
72
|
+
|
|
73
|
+
- Bucket: the slug
|
|
74
|
+
- Memos: created N, updated N, deleted N
|
|
75
|
+
- Not indexed: what and why, or "none"
|
|
76
|
+
- Status: up to date, or what is left
|