@snoai/mem-claude 1.2.5 → 1.2.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.
- package/README.md +1 -2
- package/dist/cli.js +19 -7
- package/dist/files.js +23 -12
- package/dist/install.d.ts +4 -0
- package/dist/install.js +37 -5
- package/dist/messages.d.ts +1 -0
- package/dist/messages.js +1 -0
- package/dist/worker.js +14 -4
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -29,8 +29,7 @@ start of every session.
|
|
|
29
29
|
2. Install the plugin (Node.js 22.22.3+, 24.15.0+ or 25.9.0+, and `git`):
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
-
sh -c 'sno_installer_body=$(curl -fsSL https://sno.ai/install) && printf "%s\n" "$sno_installer_body" | sh'
|
|
33
|
-
sno setup --harness claude
|
|
32
|
+
sh -c 'sno_installer_body=$(curl -fsSL https://sno.ai/install) && printf "%s\n" "$sno_installer_body" | sh' && ~/.local/bin/sno setup --harness claude
|
|
34
33
|
```
|
|
35
34
|
|
|
36
35
|
3. Open a new Claude Code session inside a git repository, then try it:
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { isAbsolute, join } from "node:path";
|
|
|
4
4
|
import { doctor } from "./doctor.js";
|
|
5
5
|
import { correctCommand, getCommand, recallCommand, rememberCommand } from "./explicit.js";
|
|
6
6
|
import { postToolUse, preToolUse, sessionEnd, sessionStart, stop, userPromptSubmit } from "./hooks.js";
|
|
7
|
-
import { installClaude } from "./install.js";
|
|
7
|
+
import { installClaude, uninstallClaude } from "./install.js";
|
|
8
8
|
import { importRepository } from "./import.js";
|
|
9
9
|
import { MESSAGES } from "./messages.js";
|
|
10
10
|
import { workspaceRoot } from "./scope.js";
|
|
@@ -20,30 +20,35 @@ function option(args, name) {
|
|
|
20
20
|
const index = args.indexOf(name);
|
|
21
21
|
return index >= 0 ? args[index + 1] : undefined;
|
|
22
22
|
}
|
|
23
|
+
const HOOK_COMMANDS = ["session-start", "user-prompt-submit", "stop", "session-end", "pre-tool-use", "post-tool-use"];
|
|
23
24
|
async function main() {
|
|
24
25
|
const [command, ...args] = process.argv.slice(2);
|
|
26
|
+
const input = HOOK_COMMANDS.includes(command ?? "") ? await readStdin() : undefined;
|
|
27
|
+
// The Cursor IDE runs Claude Code hooks with Cursor-shaped input; Sno's Cursor hooks own Cursor, so these do nothing there.
|
|
28
|
+
if (input && typeof input === "object" && "cursor_version" in input)
|
|
29
|
+
return 0;
|
|
25
30
|
if (command === "session-start") {
|
|
26
|
-
process.stdout.write(`${await sessionStart(
|
|
31
|
+
process.stdout.write(`${await sessionStart(input)}\n`);
|
|
27
32
|
return 0;
|
|
28
33
|
}
|
|
29
34
|
if (command === "user-prompt-submit") {
|
|
30
|
-
process.stdout.write(`${await userPromptSubmit(
|
|
35
|
+
process.stdout.write(`${await userPromptSubmit(input)}\n`);
|
|
31
36
|
return 0;
|
|
32
37
|
}
|
|
33
38
|
if (command === "stop") {
|
|
34
|
-
await stop(
|
|
39
|
+
await stop(input);
|
|
35
40
|
return 0;
|
|
36
41
|
}
|
|
37
42
|
if (command === "session-end") {
|
|
38
|
-
await sessionEnd(
|
|
43
|
+
await sessionEnd(input);
|
|
39
44
|
return 0;
|
|
40
45
|
}
|
|
41
46
|
if (command === "pre-tool-use") {
|
|
42
|
-
await preToolUse(
|
|
47
|
+
await preToolUse(input);
|
|
43
48
|
return 0;
|
|
44
49
|
}
|
|
45
50
|
if (command === "post-tool-use") {
|
|
46
|
-
await postToolUse(
|
|
51
|
+
await postToolUse(input);
|
|
47
52
|
return 0;
|
|
48
53
|
}
|
|
49
54
|
if (command === "worker") {
|
|
@@ -65,6 +70,13 @@ async function main() {
|
|
|
65
70
|
});
|
|
66
71
|
return 0;
|
|
67
72
|
}
|
|
73
|
+
if (command === "uninstall") {
|
|
74
|
+
const configDir = option(args, "--config-dir");
|
|
75
|
+
if (!configDir || !isAbsolute(configDir))
|
|
76
|
+
throw new Error("--config-dir must be absolute");
|
|
77
|
+
await uninstallClaude({ configDir, writeOutput: line => process.stdout.write(`${line}\n`) });
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
68
80
|
if (command === "import") {
|
|
69
81
|
const repo = option(args, "--repo");
|
|
70
82
|
if (args.includes("--user") || !repo || !isAbsolute(repo))
|
package/dist/files.js
CHANGED
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
3
|
import { chmod, mkdir, open, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname } from "node:path";
|
|
5
|
-
import
|
|
5
|
+
import { tryLock, unlock } from "fs-native-extensions";
|
|
6
6
|
export async function writeJsonAtomic(path, value) {
|
|
7
7
|
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
8
8
|
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
@@ -10,6 +10,15 @@ export async function writeJsonAtomic(path, value) {
|
|
|
10
10
|
await chmod(temporary, 0o600);
|
|
11
11
|
await rename(temporary, path);
|
|
12
12
|
}
|
|
13
|
+
function alive(pid) {
|
|
14
|
+
try {
|
|
15
|
+
process.kill(pid, 0);
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
return error instanceof Error && "code" in error && error.code === "EPERM";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
13
22
|
export async function acquirePidFileLock(path, staleAfterMs) {
|
|
14
23
|
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
15
24
|
const reclaimPath = `${path}.reclaim`;
|
|
@@ -18,7 +27,7 @@ export async function acquirePidFileLock(path, staleAfterMs) {
|
|
|
18
27
|
const closeGuard = async () => {
|
|
19
28
|
try {
|
|
20
29
|
if (held)
|
|
21
|
-
|
|
30
|
+
unlock(guard.fd);
|
|
22
31
|
}
|
|
23
32
|
finally {
|
|
24
33
|
held = false;
|
|
@@ -28,16 +37,10 @@ export async function acquirePidFileLock(path, staleAfterMs) {
|
|
|
28
37
|
try {
|
|
29
38
|
if (!(await guard.stat()).isFile())
|
|
30
39
|
throw new Error("Lock guard is not a regular file");
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
catch (error) {
|
|
36
|
-
if (error instanceof Error && "code" in error && (error.code === "EAGAIN" || error.code === "EWOULDBLOCK")) {
|
|
37
|
-
await closeGuard();
|
|
38
|
-
return undefined;
|
|
39
|
-
}
|
|
40
|
-
throw error;
|
|
40
|
+
held = tryLock(guard.fd);
|
|
41
|
+
if (!held) {
|
|
42
|
+
await closeGuard();
|
|
43
|
+
return undefined;
|
|
41
44
|
}
|
|
42
45
|
const [contents, lockStat] = await Promise.all([
|
|
43
46
|
readFile(path, "utf8").catch(() => undefined),
|
|
@@ -47,6 +50,14 @@ export async function acquirePidFileLock(path, staleAfterMs) {
|
|
|
47
50
|
await closeGuard();
|
|
48
51
|
return undefined;
|
|
49
52
|
}
|
|
53
|
+
// A holder from before an upgrade took its guard with flock, which this lock cannot see on Linux; its fresh pid
|
|
54
|
+
// file is the only sign it still runs.
|
|
55
|
+
const holder = Number(contents?.split(" ")[0]);
|
|
56
|
+
if (Number.isInteger(holder) && holder > 0 && holder !== process.pid && lockStat
|
|
57
|
+
&& Date.now() - lockStat.mtimeMs <= staleAfterMs && alive(holder)) {
|
|
58
|
+
await closeGuard();
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
50
61
|
await unlink(path).catch(error => {
|
|
51
62
|
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
52
63
|
throw error;
|
package/dist/install.d.ts
CHANGED
|
@@ -21,5 +21,9 @@ export interface InstallOptions {
|
|
|
21
21
|
export declare function isOwnedHookCommand(command: string, subcommand: string): boolean;
|
|
22
22
|
export declare function isOwnedPermissionRule(rule: string): boolean;
|
|
23
23
|
export declare function readSettings(configDir: string): Promise<Settings | undefined>;
|
|
24
|
+
export declare function uninstallClaude(options: {
|
|
25
|
+
configDir: string;
|
|
26
|
+
writeOutput: (line: string) => void;
|
|
27
|
+
}): Promise<void>;
|
|
24
28
|
export declare function installClaude(options: InstallOptions): Promise<void>;
|
|
25
29
|
export {};
|
package/dist/install.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import { basename, dirname, isAbsolute, join } from "node:path";
|
|
3
|
-
import { CODING_SKIN_HOOKS, codingSkinHookCommand, isCodingSkinHookCommand, shellQuote, } from "@snoai/memory/coding-skin";
|
|
3
|
+
import { CODING_SKIN_HOOKS, codingSkinHookCommand, isCodingSkinHookCommand, snoProgramPath, shellQuote, } from "@snoai/memory/coding-skin";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { APP_NAME } from "./constants.js";
|
|
6
6
|
import { MESSAGES } from "./messages.js";
|
|
@@ -35,7 +35,7 @@ export function isOwnedPermissionRule(rule) {
|
|
|
35
35
|
return false;
|
|
36
36
|
const body = rule.slice(5, -3);
|
|
37
37
|
if (body.endsWith(" memory"))
|
|
38
|
-
return basename(body.slice(0, -" memory".length)) === "sno";
|
|
38
|
+
return basename(snoProgramPath(body.slice(0, -" memory".length))) === "sno";
|
|
39
39
|
return isAbsolute(body) && isOwnedProgram(body);
|
|
40
40
|
}
|
|
41
41
|
export async function readSettings(configDir) {
|
|
@@ -95,9 +95,41 @@ function updateSettings(settings, programPath) {
|
|
|
95
95
|
if (index < 0)
|
|
96
96
|
allow.push(rule);
|
|
97
97
|
else
|
|
98
|
-
allow
|
|
98
|
+
allow.splice(index, allow.length - index, rule, ...allow.slice(index + 1).filter(current => !isOwnedPermissionRule(current)));
|
|
99
99
|
settings.permissions.allow = allow;
|
|
100
100
|
}
|
|
101
|
+
// Removes the hooks, rule and skill installClaude wrote; every other setting stays as it was.
|
|
102
|
+
export async function uninstallClaude(options) {
|
|
103
|
+
const settings = await readSettings(options.configDir);
|
|
104
|
+
if (!settings)
|
|
105
|
+
throw new Error(MESSAGES.settingsUnparsable);
|
|
106
|
+
const before = JSON.stringify(settings);
|
|
107
|
+
for (const event of Object.keys(CODING_SKIN_HOOKS)) {
|
|
108
|
+
const groups = settings.hooks?.[event];
|
|
109
|
+
if (!settings.hooks || !groups)
|
|
110
|
+
continue;
|
|
111
|
+
const kept = groups.map(group => ({ ...group, hooks: group.hooks.filter(hook => !hook.command || !isOwnedHookCommand(hook.command, CODING_SKIN_HOOKS[event].subcommand)) }))
|
|
112
|
+
.filter(group => group.hooks.length > 0);
|
|
113
|
+
if (kept.length)
|
|
114
|
+
settings.hooks[event] = kept;
|
|
115
|
+
else
|
|
116
|
+
delete settings.hooks[event];
|
|
117
|
+
}
|
|
118
|
+
if (settings.hooks && Object.keys(settings.hooks).length === 0)
|
|
119
|
+
delete settings.hooks;
|
|
120
|
+
if (settings.permissions?.allow) {
|
|
121
|
+
settings.permissions.allow = settings.permissions.allow.filter(rule => !isOwnedPermissionRule(rule));
|
|
122
|
+
if (settings.permissions.allow.length === 0)
|
|
123
|
+
delete settings.permissions.allow;
|
|
124
|
+
if (Object.keys(settings.permissions).length === 0)
|
|
125
|
+
delete settings.permissions;
|
|
126
|
+
}
|
|
127
|
+
if (JSON.stringify(settings) !== before) {
|
|
128
|
+
await writeFile(join(options.configDir, "settings.json"), `${JSON.stringify(settings, null, 2)}\n`, { mode: 0o600 });
|
|
129
|
+
}
|
|
130
|
+
await rm(join(options.configDir, "skills", APP_NAME), { recursive: true, force: true });
|
|
131
|
+
options.writeOutput(MESSAGES.uninstallComplete);
|
|
132
|
+
}
|
|
101
133
|
export async function installClaude(options) {
|
|
102
134
|
const settings = await readSettings(options.configDir);
|
|
103
135
|
const writes = [{
|
|
@@ -105,7 +137,7 @@ export async function installClaude(options) {
|
|
|
105
137
|
content: await readFile(new URL(`../skills/${APP_NAME}/SKILL.md`, import.meta.url), "utf8"),
|
|
106
138
|
}];
|
|
107
139
|
if (settings) {
|
|
108
|
-
updateSettings(settings, options.programPath);
|
|
140
|
+
updateSettings(settings, snoProgramPath(options.programPath));
|
|
109
141
|
writes.unshift({
|
|
110
142
|
path: join(options.configDir, "settings.json"),
|
|
111
143
|
content: `${JSON.stringify(settings, null, 2)}\n`,
|
package/dist/messages.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export declare const MESSAGES: {
|
|
|
2
2
|
readonly usage: "Run through sno: sno memory <recall|get|remember|correct|import|doctor> --harness claude, or sno memory hook <event> --harness claude";
|
|
3
3
|
readonly dryRunPrefix: "would write";
|
|
4
4
|
readonly installComplete: "Sno memory for Claude Code installed";
|
|
5
|
+
readonly uninstallComplete: "Sno memory for Claude Code removed";
|
|
5
6
|
readonly settingsUnparsable: "settings.json parse error; settings preserved";
|
|
6
7
|
readonly importCaptureDisabled: "Automatic capture is off in settings.json (capture.ambient); imported notes will be skipped.";
|
|
7
8
|
readonly doctorUnavailable: "doctor: unavailable";
|
package/dist/messages.js
CHANGED
|
@@ -2,6 +2,7 @@ export const MESSAGES = {
|
|
|
2
2
|
usage: "Run through sno: sno memory <recall|get|remember|correct|import|doctor> --harness claude, or sno memory hook <event> --harness claude",
|
|
3
3
|
dryRunPrefix: "would write",
|
|
4
4
|
installComplete: "Sno memory for Claude Code installed",
|
|
5
|
+
uninstallComplete: "Sno memory for Claude Code removed",
|
|
5
6
|
settingsUnparsable: "settings.json parse error; settings preserved",
|
|
6
7
|
importCaptureDisabled: "Automatic capture is off in settings.json (capture.ambient); imported notes will be skipped.",
|
|
7
8
|
doctorUnavailable: "doctor: unavailable",
|
package/dist/worker.js
CHANGED
|
@@ -5,7 +5,7 @@ import { createServer } from "node:http";
|
|
|
5
5
|
import { userInfo } from "node:os";
|
|
6
6
|
import { readFile, readdir, unlink } from "node:fs/promises";
|
|
7
7
|
import { join } from "node:path";
|
|
8
|
-
import { CODING_SKIN_CHILD_DEADLINE_MS, CODING_SKIN_CHILD_MAX_BUFFER_BYTES, CODING_SKIN_IMPORT_INTERVAL_MS, CODING_SKIN_MAX_ATTEMPTS, CODING_SKIN_RETRY_DELAYS_MS, CODING_SKIN_WORKER_LIFETIME_MS, HOST_MODEL_CALLBACK_HOST, HOST_MODEL_CALLBACK_PATH, } from "@snoai/memory/coding-skin";
|
|
8
|
+
import { CODING_SKIN_CHILD_DEADLINE_MS, CODING_SKIN_CHILD_MAX_BUFFER_BYTES, CODING_SKIN_IMPORT_INTERVAL_MS, CODING_SKIN_MAX_ATTEMPTS, CODING_SKIN_RETRY_DELAYS_MS, CODING_SKIN_WORKER_LIFETIME_MS, HOST_MODEL_CALLBACK_HOST, HOST_MODEL_CALLBACK_PATH, HOST_MODEL_DEADLINE_HEADER, } from "@snoai/memory/coding-skin";
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
import { MODEL_ID, SKIN_ID } from "./constants.js";
|
|
11
11
|
import { acquirePidFileLock, writeJsonAtomic } from "./files.js";
|
|
@@ -133,6 +133,14 @@ async function callbackServer(credential, activeCwd, isImport, onCall, runChild)
|
|
|
133
133
|
return;
|
|
134
134
|
}
|
|
135
135
|
onCall();
|
|
136
|
+
// Calls run one at a time and a child blocks the event loop, so a requester may have stopped waiting before
|
|
137
|
+
// this call's turn: skip it instead of spending a model run on an answer nobody reads.
|
|
138
|
+
const deadline = Number(request.headers[HOST_MODEL_DEADLINE_HEADER]);
|
|
139
|
+
if (Number.isFinite(deadline) && deadline <= Date.now()) {
|
|
140
|
+
console.log(JSON.stringify({ event: "model-callback-expired", model: requestBody.model }));
|
|
141
|
+
response.writeHead(504).end(JSON.stringify({ error: { kind: "cancelled", reason: "deadline" } }));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
136
144
|
if (isImport() && lastImportChildAt > 0) {
|
|
137
145
|
const remaining = CODING_SKIN_IMPORT_INTERVAL_MS - (Date.now() - lastImportChildAt);
|
|
138
146
|
if (remaining > 0)
|
|
@@ -142,7 +150,7 @@ async function callbackServer(credential, activeCwd, isImport, onCall, runChild)
|
|
|
142
150
|
lastImportChildAt = Date.now();
|
|
143
151
|
console.log(JSON.stringify({ event: "import-child-start", at: lastImportChildAt }));
|
|
144
152
|
}
|
|
145
|
-
const result = await runChild(requestBody.prompt, cwd);
|
|
153
|
+
const result = await runChild(requestBody.prompt, cwd, Number.isFinite(deadline) ? deadline - Date.now() : undefined);
|
|
146
154
|
const status = result.kind === "ok" ? 200 : result.kind === "cancelled" ? 504 : 503;
|
|
147
155
|
console.log(JSON.stringify({ event: "model-callback", model: requestBody.model, status }));
|
|
148
156
|
response.setHeader("content-type", "application/json");
|
|
@@ -208,7 +216,7 @@ export async function runWorker(dependencies = productionDependencies()) {
|
|
|
208
216
|
let handoff = false;
|
|
209
217
|
try {
|
|
210
218
|
mkdirSync(childCwd, { recursive: true, mode: 0o700 });
|
|
211
|
-
const callback = await callbackServer(credential, () => currentCwd ?? childCwd, () => currentImport, () => { lastCallbackAt = now(); }, (prompt, cwd) => dependencies.runChild(prompt, cwd, Math.min(CODING_SKIN_CHILD_DEADLINE_MS, CODING_SKIN_WORKER_LIFETIME_MS - (now() - workerStartedAt))));
|
|
219
|
+
const callback = await callbackServer(credential, () => currentCwd ?? childCwd, () => currentImport, () => { lastCallbackAt = now(); }, (prompt, cwd, requesterMs) => dependencies.runChild(prompt, cwd, Math.min(CODING_SKIN_CHILD_DEADLINE_MS, CODING_SKIN_WORKER_LIFETIME_MS - (now() - workerStartedAt), requesterMs ?? Infinity)));
|
|
212
220
|
server = callback.server;
|
|
213
221
|
const client = await dependencies.connect();
|
|
214
222
|
const principal = client.principal || userInfo().username;
|
|
@@ -263,9 +271,11 @@ export async function runWorker(dependencies = productionDependencies()) {
|
|
|
263
271
|
await unlink(path);
|
|
264
272
|
break;
|
|
265
273
|
}
|
|
266
|
-
catch {
|
|
274
|
+
catch (error) {
|
|
267
275
|
await updateImportReceipt(record.importReceipt, "failures");
|
|
268
276
|
const attempts = record.attempts + 1;
|
|
277
|
+
console.log(JSON.stringify({ event: "capture-failed", turnId: record.turnId, attempt: attempts,
|
|
278
|
+
final: attempts >= CODING_SKIN_MAX_ATTEMPTS, error: error instanceof Error ? error.message : String(error) }));
|
|
269
279
|
const retryDelay = CODING_SKIN_RETRY_DELAYS_MS[attempts - 1];
|
|
270
280
|
record = {
|
|
271
281
|
...record,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@snoai/mem-claude",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.7",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"publishConfig": { "access": "public" },
|
|
6
6
|
"repository": {
|
|
@@ -26,11 +26,10 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@snoai/memory": "^1.2.0",
|
|
29
|
-
"fs-
|
|
29
|
+
"fs-native-extensions": "^1.5.1",
|
|
30
30
|
"zod": "^4.5.4"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@types/fs-ext": "^2.0.3",
|
|
34
33
|
"@types/node": "^22.19.17",
|
|
35
34
|
"typescript": "^7.0.2"
|
|
36
35
|
}
|