gigarag-cursor 0.3.0 → 0.3.2
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/.cursor-plugin/plugin.json +1 -1
- package/cli/cli.js +47 -1
- package/cli/commands/indexSync.js +11 -1
- package/cli/commands/status.js +4 -0
- package/cli/hooks.js +4 -0
- package/cli/package.json +3 -2
- package/cli/update.js +197 -0
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gigarag",
|
|
3
3
|
"description": "Your GigaRAG knowledge base inside Cursor: index a codebase, save decisions, recall them later.",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.2",
|
|
5
5
|
"author": { "name": "GigaRAG", "url": "https://gigarag.com" },
|
|
6
6
|
"homepage": "https://gigarag.com/connect/cursor",
|
|
7
7
|
"license": "UNLICENSED",
|
package/cli/cli.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
1
3
|
import { version } from './constants.js';
|
|
4
|
+
import { stateDir } from './paths.js';
|
|
5
|
+
import { tryOpenState } from './state.js';
|
|
2
6
|
import { err, out, UsageError } from './ui.js';
|
|
3
7
|
const HELP = `gigarag ${version()}
|
|
4
8
|
|
|
@@ -41,10 +45,47 @@ const COMMANDS = {
|
|
|
41
45
|
'auth-header': async () => (await import('./commands/authHeader.js')).authHeader,
|
|
42
46
|
'index-sync': async () => (await import('./commands/indexSync.js')).indexSync,
|
|
43
47
|
};
|
|
48
|
+
/**
|
|
49
|
+
* Commands a person reads, never a script: the ones whose stdout `gigarag hook`, `mcp`, `index-sync`,
|
|
50
|
+
* `record` and `scan` are read by something else stays clear of. `status` also stays clear of it: it
|
|
51
|
+
* already puts the same notice on its own stdout, and printing it again on stderr would just repeat
|
|
52
|
+
* it. Each of the rest gets one line on stderr, after it runs, when a newer version is available.
|
|
53
|
+
*/
|
|
54
|
+
const INTERACTIVE_COMMANDS = new Set(['login', 'logout', 'connect', 'workspaces', 'use', 'trust']);
|
|
55
|
+
/**
|
|
56
|
+
* Prints the one-line update notice, or nothing at all: not signed in, never checked, already
|
|
57
|
+
* current, or stderr isn't a terminal a person is reading (a pipe, a redirect, another program
|
|
58
|
+
* capturing it). `update.js` and the state it reads are loaded here, on demand, so a hook or the
|
|
59
|
+
* MCP bridge, which never call this, never pay to load them.
|
|
60
|
+
*/
|
|
61
|
+
export async function notifyIfOutdated() {
|
|
62
|
+
if (!process.stderr.isTTY)
|
|
63
|
+
return;
|
|
64
|
+
try {
|
|
65
|
+
if (!existsSync(join(stateDir(), 'state.db')))
|
|
66
|
+
return;
|
|
67
|
+
const state = tryOpenState();
|
|
68
|
+
if (!state)
|
|
69
|
+
return;
|
|
70
|
+
try {
|
|
71
|
+
const { terminalNotice } = await import('./update.js');
|
|
72
|
+
const notice = terminalNotice(state);
|
|
73
|
+
if (notice)
|
|
74
|
+
err(notice);
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
state.close();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
/* a command's own result is never held up by this */
|
|
82
|
+
}
|
|
83
|
+
}
|
|
44
84
|
export async function main(argv) {
|
|
45
85
|
const [command, ...rest] = argv;
|
|
46
86
|
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
47
87
|
out(HELP);
|
|
88
|
+
await notifyIfOutdated();
|
|
48
89
|
return 0;
|
|
49
90
|
}
|
|
50
91
|
if (command === '--version' || command === '-v' || command === 'version') {
|
|
@@ -60,7 +101,12 @@ export async function main(argv) {
|
|
|
60
101
|
return 2;
|
|
61
102
|
}
|
|
62
103
|
try {
|
|
63
|
-
|
|
104
|
+
const code = await (await load())(rest);
|
|
105
|
+
// Only after the command actually succeeded: a command that threw never gets this line, since
|
|
106
|
+
// whatever it failed to do is the thing that needs attention, not a version bump.
|
|
107
|
+
if (INTERACTIVE_COMMANDS.has(command))
|
|
108
|
+
await notifyIfOutdated();
|
|
109
|
+
return code;
|
|
64
110
|
}
|
|
65
111
|
catch (e) {
|
|
66
112
|
if (e instanceof UsageError) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util';
|
|
2
2
|
import { GigaRag } from '../sdk.js';
|
|
3
3
|
import { State } from '../state.js';
|
|
4
|
+
import { checkForUpdate } from '../update.js';
|
|
4
5
|
const MAX_NODES = 5000;
|
|
5
6
|
/** A worker that started less than this long ago owns the sync. */
|
|
6
7
|
const LOCK_MS = 2 * 60_000;
|
|
@@ -23,7 +24,7 @@ async function pendingImports(rag) {
|
|
|
23
24
|
* so it is what makes memos saved from other clients, a chat app or another
|
|
24
25
|
* machine, show up in the next session. Runs detached from a hook, never in one.
|
|
25
26
|
*/
|
|
26
|
-
export async function indexSync(argv = []) {
|
|
27
|
+
export async function indexSync(argv = [], opts = {}) {
|
|
27
28
|
const { values } = parseArgs({ args: argv, options: { workspace: { type: 'string' } } });
|
|
28
29
|
const workspace = values.workspace;
|
|
29
30
|
const state = new State();
|
|
@@ -33,6 +34,15 @@ export async function indexSync(argv = []) {
|
|
|
33
34
|
held = state.tryAcquire('sync_lock', LOCK_MS);
|
|
34
35
|
if (!held)
|
|
35
36
|
return 0;
|
|
37
|
+
// Never lets an update check turn a real sync failure into something else, or the reverse:
|
|
38
|
+
// a throwing or hanging registry never keeps the memo sync below from running or from
|
|
39
|
+
// recording that it finished.
|
|
40
|
+
try {
|
|
41
|
+
await checkForUpdate(state, { fetch: opts.fetch });
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
/* an update check is not worth failing a sync over */
|
|
45
|
+
}
|
|
36
46
|
const rag = new GigaRag(workspace ? { workspaceId: workspace } : {});
|
|
37
47
|
const rows = [];
|
|
38
48
|
for await (const n of rag.iterateNodes({ max: MAX_NODES })) {
|
package/cli/commands/status.js
CHANGED
|
@@ -8,6 +8,7 @@ import { createClient, resolveUrl } from '../mcp/session.js';
|
|
|
8
8
|
import { McpHttpError } from '../mcp/client.js';
|
|
9
9
|
import { tildify } from '../paths.js';
|
|
10
10
|
import { tryOpenState } from '../state.js';
|
|
11
|
+
import { terminalNotice } from '../update.js';
|
|
11
12
|
import { out, table } from '../ui.js';
|
|
12
13
|
import { existsSync } from 'node:fs';
|
|
13
14
|
import { join } from 'node:path';
|
|
@@ -74,6 +75,7 @@ export async function status(argv) {
|
|
|
74
75
|
try {
|
|
75
76
|
const last = state.getMeta('last_sync');
|
|
76
77
|
report.index = { memos: state.nodeCount(), ...(last ? { lastSync: new Date(Number(last)).toISOString() } : {}) };
|
|
78
|
+
report.update = terminalNotice(state);
|
|
77
79
|
for (const r of state.listRepos()) {
|
|
78
80
|
report.repos.push({
|
|
79
81
|
slug: r.slug,
|
|
@@ -124,6 +126,8 @@ function print(r, missing) {
|
|
|
124
126
|
if (r.index) {
|
|
125
127
|
out(`\nMemo index: ${r.index.memos} memos${r.index.lastSync ? `, synced ${r.index.lastSync}` : ', not synced yet'}`);
|
|
126
128
|
}
|
|
129
|
+
if (r.update)
|
|
130
|
+
out(`\nUpdate available: ${r.update}`);
|
|
127
131
|
if (r.repos.length > 0) {
|
|
128
132
|
out('\nIndexed repositories:');
|
|
129
133
|
for (const line of table(r.repos.map(p => [` ${p.slug}`, p.indexedAt ? `indexed ${p.indexedAt}` : 'never indexed', p.dirty ? `${p.dirty} changed since` : '']))) {
|
package/cli/hooks.js
CHANGED
|
@@ -11,6 +11,7 @@ import { findRepoRoot, repoInfo } from './scan/repo.js';
|
|
|
11
11
|
import { tryOpenState } from './state.js';
|
|
12
12
|
import { maxRewriteFiles, readNote, renderNote } from './sync/notes.js';
|
|
13
13
|
import { syncSlug } from './sync/slug.js';
|
|
14
|
+
import { sessionStartNotice } from './update.js';
|
|
14
15
|
/** Rows the SessionStart index carries at most. */
|
|
15
16
|
export const INDEX_ROWS = 40;
|
|
16
17
|
/** A sync worker is started at the end of a session only if the last one is older than this. */
|
|
@@ -73,6 +74,8 @@ export function renderIndex(rows, total, dirty, repoSlug, extras = {}) {
|
|
|
73
74
|
? '1 imported item is waiting to be filed. /gigainbox files it.'
|
|
74
75
|
: `${pending} imported items are waiting to be filed. /gigainbox files them.`);
|
|
75
76
|
}
|
|
77
|
+
if (extras.update)
|
|
78
|
+
lines.push(extras.update);
|
|
76
79
|
return lines.join('\n');
|
|
77
80
|
}
|
|
78
81
|
export async function runHook(rawEvent, raw, dir = stateDir()) {
|
|
@@ -134,6 +137,7 @@ function sessionStart(state, input, cursor) {
|
|
|
134
137
|
const text = renderIndex(state.recentNodes(INDEX_ROWS, ws), state.nodeCount(ws), state.hasRepo(slug) ? state.dirtyCount(slug) : 0, slug, {
|
|
135
138
|
note: noteLine,
|
|
136
139
|
importsPending,
|
|
140
|
+
update: sessionStartNotice(state),
|
|
137
141
|
});
|
|
138
142
|
if (!process.env['GIGARAG_SYNC_WORKER'])
|
|
139
143
|
maybeSpawnSync(state, ws);
|
package/cli/package.json
CHANGED
package/cli/update.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { isSea } from 'node:sea';
|
|
5
|
+
import { version } from './constants.js';
|
|
6
|
+
/** Where the CLI's own npm package answers with the newest published version. */
|
|
7
|
+
export const REGISTRY_URL = 'https://registry.npmjs.org/gigarag/latest';
|
|
8
|
+
/** Where the Claude Code plugin's npm package answers with the newest published version. */
|
|
9
|
+
export const PLUGIN_REGISTRY_URL = 'https://registry.npmjs.org/gigarag-claude-code/latest';
|
|
10
|
+
/** How long a checked timestamp holds off the next attempt, success or failure alike. */
|
|
11
|
+
export const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
12
|
+
/** How long the registry request is given before it counts as a failure. */
|
|
13
|
+
export const CHECK_TIMEOUT_MS = 3000;
|
|
14
|
+
/** How long a SessionStart line, once shown for a given version, stays quiet before it can show again. */
|
|
15
|
+
export const NOTICE_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
16
|
+
const CHECKED_AT_KEY = 'update_checked_at';
|
|
17
|
+
const LATEST_KEY = 'latest_version';
|
|
18
|
+
const PLUGIN_LATEST_KEY = 'latest_plugin_version';
|
|
19
|
+
/** What a version is allowed to look like before it is trusted: no prerelease, no build metadata, three parts, each capped so a hostile answer can't grow unbounded. */
|
|
20
|
+
const PLAIN_VERSION = /^\d{1,6}\.\d{1,6}\.\d{1,6}$/;
|
|
21
|
+
/**
|
|
22
|
+
* True when the check is turned off for this run: an opt-out, or a CI environment nobody is
|
|
23
|
+
* watching. `CI=false`, which some runners set deliberately to mean "this is not CI", never
|
|
24
|
+
* disables it: only a truthy value that isn't the literal string "false" does.
|
|
25
|
+
*/
|
|
26
|
+
export function updateCheckDisabled(env = process.env) {
|
|
27
|
+
return Boolean(env['GIGARAG_NO_UPDATE_CHECK']) || Boolean(env['CI'] && env['CI'] !== 'false');
|
|
28
|
+
}
|
|
29
|
+
async function fetchVersion(doFetch, url, timeoutMs) {
|
|
30
|
+
try {
|
|
31
|
+
const res = await doFetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
32
|
+
if (!res.ok)
|
|
33
|
+
return undefined;
|
|
34
|
+
const body = (await res.json());
|
|
35
|
+
const v = typeof body.version === 'string' ? body.version : '';
|
|
36
|
+
return PLAIN_VERSION.test(v) ? v : undefined;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Asks npm for the newest published version of both `gigarag` and `gigarag-claude-code`, at most
|
|
44
|
+
* once a day, and remembers what it heard, each under its own key. Runs inside the detached
|
|
45
|
+
* `index-sync` worker, never in a hook, so a slow or blocked network never touches a session's
|
|
46
|
+
* budget. The checked timestamp is written before either request, so a machine that cannot reach
|
|
47
|
+
* the registry is not retried on every worker run; each version is written only on a clean,
|
|
48
|
+
* well-shaped answer for that package, independently of the other. A bad status, a body that is
|
|
49
|
+
* not a plain x.y.z, a timeout, a thrown error stores nothing beyond the attempt for that package
|
|
50
|
+
* and is otherwise silent.
|
|
51
|
+
*/
|
|
52
|
+
export async function checkForUpdate(state, opts = {}) {
|
|
53
|
+
if (updateCheckDisabled(opts.env ?? process.env))
|
|
54
|
+
return;
|
|
55
|
+
const now = opts.now ?? Date.now();
|
|
56
|
+
const last = Number(state.getMeta(CHECKED_AT_KEY) ?? 0);
|
|
57
|
+
// A clock that jumped backward (a stored timestamp now in the future) never holds off a check:
|
|
58
|
+
// only a `last` that is genuinely in the past and inside the interval does.
|
|
59
|
+
if (last <= now && now - last < CHECK_INTERVAL_MS)
|
|
60
|
+
return;
|
|
61
|
+
state.setMeta(CHECKED_AT_KEY, String(now));
|
|
62
|
+
const doFetch = opts.fetch ?? fetch;
|
|
63
|
+
const timeoutMs = opts.timeoutMs ?? CHECK_TIMEOUT_MS;
|
|
64
|
+
const [cliVersion, pluginVersion] = await Promise.all([
|
|
65
|
+
fetchVersion(doFetch, opts.url ?? REGISTRY_URL, timeoutMs),
|
|
66
|
+
fetchVersion(doFetch, opts.pluginUrl ?? PLUGIN_REGISTRY_URL, timeoutMs),
|
|
67
|
+
]);
|
|
68
|
+
if (cliVersion)
|
|
69
|
+
state.setMeta(LATEST_KEY, cliVersion);
|
|
70
|
+
if (pluginVersion)
|
|
71
|
+
state.setMeta(PLUGIN_LATEST_KEY, pluginVersion);
|
|
72
|
+
}
|
|
73
|
+
/** Numeric major.minor.patch compare. A prerelease or anything else that is not a plain x.y.z never prompts, on either side. */
|
|
74
|
+
export function isNewerVersion(current, latest) {
|
|
75
|
+
if (!PLAIN_VERSION.test(current) || !PLAIN_VERSION.test(latest))
|
|
76
|
+
return false;
|
|
77
|
+
const a = current.split('.').map(Number);
|
|
78
|
+
const b = latest.split('.').map(Number);
|
|
79
|
+
for (let i = 0; i < 3; i++) {
|
|
80
|
+
if (b[i] > a[i])
|
|
81
|
+
return true;
|
|
82
|
+
if (b[i] < a[i])
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The CLI version the worker last stored against `gigarag`, when it is validly shaped and strictly
|
|
89
|
+
* newer than this build. Undefined whenever the check itself is disabled, so the opt-out also
|
|
90
|
+
* silences a notice a still-running worker stored before the variable was set.
|
|
91
|
+
*/
|
|
92
|
+
export function pendingUpdate(state, env = process.env) {
|
|
93
|
+
if (updateCheckDisabled(env))
|
|
94
|
+
return undefined;
|
|
95
|
+
const latest = state.getMeta(LATEST_KEY);
|
|
96
|
+
if (!latest || !PLAIN_VERSION.test(latest))
|
|
97
|
+
return undefined;
|
|
98
|
+
return isNewerVersion(version(), latest) ? latest : undefined;
|
|
99
|
+
}
|
|
100
|
+
/** The plugin version the worker last stored against `gigarag-claude-code`, under the same rules as {@link pendingUpdate}. */
|
|
101
|
+
export function pendingPluginUpdate(state, env = process.env) {
|
|
102
|
+
if (updateCheckDisabled(env))
|
|
103
|
+
return undefined;
|
|
104
|
+
const latest = state.getMeta(PLUGIN_LATEST_KEY);
|
|
105
|
+
if (!latest || !PLAIN_VERSION.test(latest))
|
|
106
|
+
return undefined;
|
|
107
|
+
return isNewerVersion(version(), latest) ? latest : undefined;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* How this copy of the CLI reached the machine, so a notice names the right next step.
|
|
111
|
+
* `isSea()` is the standalone binary. The plugin's vendored copy is the one path
|
|
112
|
+
* `scripts/vendor-cli.mjs` always writes to, a `cli/` folder holding `bin.js` directly;
|
|
113
|
+
* anything else is the npm package, global, local, or run once through `npx`.
|
|
114
|
+
*/
|
|
115
|
+
export function detectInstallKind(binPath = fileURLToPath(new URL('./bin.js', import.meta.url)), sea = isSea()) {
|
|
116
|
+
if (sea)
|
|
117
|
+
return 'binary';
|
|
118
|
+
return basename(dirname(binPath)) === 'cli' ? 'plugin' : 'npm';
|
|
119
|
+
}
|
|
120
|
+
/** True when this run's own path runs through npm's `_npx` cache, so the npm suggestion should name `npx` rather than a global install. */
|
|
121
|
+
export function isNpxInstall(binPath = fileURLToPath(new URL('./bin.js', import.meta.url))) {
|
|
122
|
+
return /[\\/]_npx[\\/]/.test(binPath);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Which plugin vendored this copy, read from the `gigaragHost` field `scripts/vendor-cli.mjs`
|
|
126
|
+
* writes into the vendored `package.json` next to `bin.js`. Undefined for anything without that
|
|
127
|
+
* field: an npm install, a standalone binary, or a copy without it is treated as not Claude Code.
|
|
128
|
+
*/
|
|
129
|
+
function pluginHost(binPath) {
|
|
130
|
+
try {
|
|
131
|
+
const pkg = JSON.parse(readFileSync(join(dirname(binPath), 'package.json'), 'utf8'));
|
|
132
|
+
return typeof pkg.gigaragHost === 'string' ? pkg.gigaragHost : undefined;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Whether this copy is the Claude Code plugin's own vendored copy, deciding the SessionStart
|
|
140
|
+
* wording. `gigaragHost` decides it, and only it: `scripts/vendor-cli.mjs` always writes the field,
|
|
141
|
+
* so there is no real vendored copy without one to fall back for, and a path-shape guess would only
|
|
142
|
+
* add a second, weaker way to answer the same question. Its absence, an npm install or a standalone
|
|
143
|
+
* binary, is "not Claude Code".
|
|
144
|
+
*/
|
|
145
|
+
function isClaudeCodeHost(binPath) {
|
|
146
|
+
return pluginHost(binPath) === 'claude-code';
|
|
147
|
+
}
|
|
148
|
+
/** The next action for the install kind this copy is running from. */
|
|
149
|
+
export function updateCommand(kind, npx = false) {
|
|
150
|
+
switch (kind) {
|
|
151
|
+
case 'binary':
|
|
152
|
+
return ('Run: curl -fsSL https://gigarag.com/install | GIGARAG_NO_SETUP=1 sh ' +
|
|
153
|
+
'(Windows PowerShell: $env:GIGARAG_NO_SETUP=1; irm https://gigarag.com/install.ps1 | iex)');
|
|
154
|
+
case 'plugin':
|
|
155
|
+
return 'Run: claude plugin marketplace update gigarag, then claude plugin update gigarag@gigarag, and restart Claude Code';
|
|
156
|
+
default:
|
|
157
|
+
return npx ? 'Run: npx gigarag@latest' : 'Run: npm install -g gigarag@latest';
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/** One line for an interactive command's stderr, or undefined when this copy is current. */
|
|
161
|
+
export function terminalNotice(state, env = process.env) {
|
|
162
|
+
const latest = pendingUpdate(state, env);
|
|
163
|
+
if (!latest)
|
|
164
|
+
return undefined;
|
|
165
|
+
const binPath = fileURLToPath(new URL('./bin.js', import.meta.url));
|
|
166
|
+
return `gigarag ${version()} is out of date; ${latest} is available. ${updateCommand(detectInstallKind(binPath), isNpxInstall(binPath))}`;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The one line SessionStart adds for the agent, at most once every 24 hours for a given target
|
|
170
|
+
* version, so a session that starts every few minutes doesn't repeat itself. The hook only ever
|
|
171
|
+
* runs from a plugin's own vendored copy, so the next action is always the plugin's, not a guess
|
|
172
|
+
* at how this machine installed the CLI proper; a Cursor or Copilot vendored copy, or anything else
|
|
173
|
+
* that isn't the Claude Code plugin's own copy (see {@link isClaudeCodeHost}), never gets the
|
|
174
|
+
* Claude Code wording.
|
|
175
|
+
*/
|
|
176
|
+
export function sessionStartNotice(state, now = Date.now(), binPath = fileURLToPath(new URL('./bin.js', import.meta.url)), env = process.env) {
|
|
177
|
+
if (!isClaudeCodeHost(binPath))
|
|
178
|
+
return undefined;
|
|
179
|
+
const latest = pendingPluginUpdate(state, env);
|
|
180
|
+
if (!latest)
|
|
181
|
+
return undefined;
|
|
182
|
+
const shownKey = `update_notified:${latest}`;
|
|
183
|
+
const last = Number(state.getMeta(shownKey) ?? 0);
|
|
184
|
+
if (last <= now && now - last < NOTICE_INTERVAL_MS)
|
|
185
|
+
return undefined;
|
|
186
|
+
// A write that can't happen right now (SQLITE_BUSY, a concurrent writer) must never cost the
|
|
187
|
+
// agent the rest of the SessionStart index over one throttling timestamp; worst case, the line
|
|
188
|
+
// repeats a little sooner than every 24 hours next time.
|
|
189
|
+
try {
|
|
190
|
+
state.setMeta(shownKey, String(now));
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
/* the memo index this is appended to matters far more than this one timestamp */
|
|
194
|
+
}
|
|
195
|
+
return (`GigaRAG plugin ${version()} is installed and ${latest} is available. Ask the user whether to update: run ` +
|
|
196
|
+
'`claude plugin marketplace update gigarag` then `claude plugin update gigarag@gigarag`, and restart Claude Code.');
|
|
197
|
+
}
|