@oratis/lisa 0.3.0 → 0.4.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 +34 -5
- package/README.zh-CN.md +34 -5
- package/dist/advisor/detectors.d.ts +25 -0
- package/dist/advisor/detectors.d.ts.map +1 -0
- package/dist/advisor/detectors.js +161 -0
- package/dist/advisor/detectors.js.map +1 -0
- package/dist/advisor/engine.d.ts +41 -0
- package/dist/advisor/engine.d.ts.map +1 -0
- package/dist/advisor/engine.js +156 -0
- package/dist/advisor/engine.js.map +1 -0
- package/dist/advisor/types.d.ts +68 -0
- package/dist/advisor/types.d.ts.map +1 -0
- package/dist/advisor/types.js +19 -0
- package/dist/advisor/types.js.map +1 -0
- package/dist/channels/imessage.d.ts +16 -0
- package/dist/channels/imessage.d.ts.map +1 -1
- package/dist/channels/imessage.js +29 -8
- package/dist/channels/imessage.js.map +1 -1
- package/dist/heartbeat/config.d.ts +10 -0
- package/dist/heartbeat/config.d.ts.map +1 -1
- package/dist/heartbeat/config.js +8 -2
- package/dist/heartbeat/config.js.map +1 -1
- package/dist/heartbeat/runner.d.ts.map +1 -1
- package/dist/heartbeat/runner.js +30 -0
- package/dist/heartbeat/runner.js.map +1 -1
- package/dist/integrations/claude-code/observer.d.ts +19 -0
- package/dist/integrations/claude-code/observer.d.ts.map +1 -0
- package/dist/integrations/claude-code/observer.js +59 -0
- package/dist/integrations/claude-code/observer.js.map +1 -0
- package/dist/integrations/claude-code/parser.d.ts +2 -0
- package/dist/integrations/claude-code/parser.d.ts.map +1 -1
- package/dist/integrations/claude-code/parser.js +153 -0
- package/dist/integrations/claude-code/parser.js.map +1 -1
- package/dist/integrations/claude-code/watcher.d.ts +9 -0
- package/dist/integrations/claude-code/watcher.d.ts.map +1 -1
- package/dist/integrations/claude-code/watcher.js +25 -4
- package/dist/integrations/claude-code/watcher.js.map +1 -1
- package/dist/integrations/codex/observer.d.ts +48 -0
- package/dist/integrations/codex/observer.d.ts.map +1 -0
- package/dist/integrations/codex/observer.js +213 -0
- package/dist/integrations/codex/observer.js.map +1 -0
- package/dist/integrations/current-hub.d.ts +13 -0
- package/dist/integrations/current-hub.d.ts.map +1 -0
- package/dist/integrations/current-hub.js +17 -0
- package/dist/integrations/current-hub.js.map +1 -0
- package/dist/integrations/hub.d.ts +45 -0
- package/dist/integrations/hub.d.ts.map +1 -0
- package/dist/integrations/hub.js +110 -0
- package/dist/integrations/hub.js.map +1 -0
- package/dist/integrations/registry.d.ts +15 -0
- package/dist/integrations/registry.d.ts.map +1 -0
- package/dist/integrations/registry.js +38 -0
- package/dist/integrations/registry.js.map +1 -0
- package/dist/integrations/types.d.ts +90 -0
- package/dist/integrations/types.d.ts.map +1 -0
- package/dist/integrations/types.js +12 -0
- package/dist/integrations/types.js.map +1 -0
- package/dist/memory/vector.d.ts +5 -1
- package/dist/memory/vector.d.ts.map +1 -1
- package/dist/memory/vector.js +58 -1
- package/dist/memory/vector.js.map +1 -1
- package/dist/soul/lock.d.ts +38 -0
- package/dist/soul/lock.d.ts.map +1 -0
- package/dist/soul/lock.js +103 -0
- package/dist/soul/lock.js.map +1 -0
- package/dist/soul/paths.d.ts.map +1 -1
- package/dist/soul/paths.js +12 -6
- package/dist/soul/paths.js.map +1 -1
- package/dist/soul/slug.d.ts +40 -0
- package/dist/soul/slug.d.ts.map +1 -0
- package/dist/soul/slug.js +78 -0
- package/dist/soul/slug.js.map +1 -0
- package/dist/soul/store.d.ts +12 -1
- package/dist/soul/store.d.ts.map +1 -1
- package/dist/soul/store.js +40 -23
- package/dist/soul/store.js.map +1 -1
- package/dist/soul/tools.d.ts.map +1 -1
- package/dist/soul/tools.js +10 -3
- package/dist/soul/tools.js.map +1 -1
- package/dist/tools/advise_now.d.ts +11 -0
- package/dist/tools/advise_now.d.ts.map +1 -0
- package/dist/tools/advise_now.js +34 -0
- package/dist/tools/advise_now.js.map +1 -0
- package/dist/tools/dispatch_agent.d.ts +38 -0
- package/dist/tools/dispatch_agent.d.ts.map +1 -0
- package/dist/tools/dispatch_agent.js +145 -0
- package/dist/tools/dispatch_agent.js.map +1 -0
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/web_fetch.d.ts +10 -0
- package/dist/tools/web_fetch.d.ts.map +1 -1
- package/dist/tools/web_fetch.js +48 -16
- package/dist/tools/web_fetch.js.map +1 -1
- package/dist/web/island.d.ts +1 -1
- package/dist/web/island.d.ts.map +1 -1
- package/dist/web/island.js +69 -5
- package/dist/web/island.js.map +1 -1
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +79 -16
- package/dist/web/server.js.map +1 -1
- package/package.json +3 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration registry — mirrors src/channels/registry.ts.
|
|
3
|
+
*
|
|
4
|
+
* Each agent adapter calls registerIntegration() at module-load time; the
|
|
5
|
+
* hub looks them up by name. Keeps adapters decoupled and lets community
|
|
6
|
+
* adapters slot in by importing one more module.
|
|
7
|
+
*/
|
|
8
|
+
const FACTORIES = new Map();
|
|
9
|
+
export function registerIntegration(name, factory) {
|
|
10
|
+
FACTORIES.set(name, factory);
|
|
11
|
+
}
|
|
12
|
+
export async function makeIntegration(name, cfg) {
|
|
13
|
+
const factory = FACTORIES.get(name);
|
|
14
|
+
if (!factory) {
|
|
15
|
+
throw new Error(`unknown integration "${name}". Known: ${Array.from(FACTORIES.keys()).join(", ") || "(none registered)"}`);
|
|
16
|
+
}
|
|
17
|
+
return await factory(cfg);
|
|
18
|
+
}
|
|
19
|
+
export function listAvailableIntegrations() {
|
|
20
|
+
return Array.from(FACTORIES.keys()).sort();
|
|
21
|
+
}
|
|
22
|
+
/** Test hook — clear the registry between unit tests. */
|
|
23
|
+
export function _resetIntegrationsForTest() {
|
|
24
|
+
FACTORIES.clear();
|
|
25
|
+
builtinsRegistered = false;
|
|
26
|
+
}
|
|
27
|
+
// Lazy registration of built-in adapters. Each module calls
|
|
28
|
+
// registerIntegration() at import time.
|
|
29
|
+
let builtinsRegistered = false;
|
|
30
|
+
export async function registerBuiltinIntegrations() {
|
|
31
|
+
if (builtinsRegistered)
|
|
32
|
+
return;
|
|
33
|
+
builtinsRegistered = true;
|
|
34
|
+
await import("./claude-code/observer.js");
|
|
35
|
+
await import("./codex/observer.js");
|
|
36
|
+
// Additional adapters register here as they land (opencode, aider, …).
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/integrations/registry.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAQH,MAAM,SAAS,GAAG,IAAI,GAAG,EAAgC,CAAC;AAE1D,MAAM,UAAU,mBAAmB,CACjC,IAAY,EACZ,OAA6B;IAE7B,SAAS,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,IAAY,EACZ,GAA2B;IAE3B,MAAM,OAAO,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACpC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,wBAAwB,IAAI,aAC1B,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,mBAC7C,EAAE,CACH,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC;AAC5B,CAAC;AAED,MAAM,UAAU,yBAAyB;IACvC,OAAO,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;AAC7C,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,yBAAyB;IACvC,SAAS,CAAC,KAAK,EAAE,CAAC;IAClB,kBAAkB,GAAG,KAAK,CAAC;AAC7B,CAAC;AAED,4DAA4D;AAC5D,wCAAwC;AACxC,IAAI,kBAAkB,GAAG,KAAK,CAAC;AAC/B,MAAM,CAAC,KAAK,UAAU,2BAA2B;IAC/C,IAAI,kBAAkB;QAAE,OAAO;IAC/B,kBAAkB,GAAG,IAAI,CAAC;IAC1B,MAAM,MAAM,CAAC,2BAA2B,CAAC,CAAC;IAC1C,MAAM,MAAM,CAAC,qBAAqB,CAAC,CAAC;IACpC,uEAAuE;AACzE,CAAC"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-agent orchestration — shared types.
|
|
3
|
+
*
|
|
4
|
+
* LISA observes many CLI agents (Claude Code, Codex, OpenCode, Aider, …),
|
|
5
|
+
* each with its own on-disk session format. These types are the normalized,
|
|
6
|
+
* agent-agnostic shape every adapter produces, so the UI, the hub, and the
|
|
7
|
+
* advisor never care which agent a session came from.
|
|
8
|
+
*
|
|
9
|
+
* See docs/ORCHESTRATOR_PLAN.md for the full design (L1 OBSERVE, L2 UNDERSTAND).
|
|
10
|
+
*/
|
|
11
|
+
/** Which agent produced a session. Open-ended string so community adapters
|
|
12
|
+
* can add kinds without touching this file. */
|
|
13
|
+
export type AgentKind = "claude-code" | "codex" | "opencode" | "aider" | "gemini" | "cursor" | "github-pr" | "mcp" | "lisa" | (string & {});
|
|
14
|
+
/** Normalized session state across all agents. Superset of any single
|
|
15
|
+
* agent's states; adapters map their native states onto these. */
|
|
16
|
+
export type AgentSessionState = "working" | "waiting" | "error" | "idle" | "done" | "unknown";
|
|
17
|
+
/**
|
|
18
|
+
* L2 — structural activity for a session. Populated by tier ≥ "activity"
|
|
19
|
+
* adapters. PRIVACY: every field here is structural metadata (tool NAMES,
|
|
20
|
+
* file PATHS, command argv[0], error strings, counts) — never a prompt, a
|
|
21
|
+
* model reply, or full command arguments. See parser privacy tests.
|
|
22
|
+
*/
|
|
23
|
+
export interface SessionActivity {
|
|
24
|
+
/** Number of assistant/user turns observed (rough progress indicator). */
|
|
25
|
+
turnCount: number;
|
|
26
|
+
/** Most recent tool names, newest last, e.g. ["Read","Edit","Bash"]. */
|
|
27
|
+
lastTools: string[];
|
|
28
|
+
/** File paths touched by Edit/Write-style tools (deduped, capped). */
|
|
29
|
+
filesTouched: string[];
|
|
30
|
+
/** argv[0] of the most recent shell command, e.g. "npm" — not the full command. */
|
|
31
|
+
lastCommandName?: string;
|
|
32
|
+
/** Short error summary if the last meaningful activity errored. */
|
|
33
|
+
lastError?: string;
|
|
34
|
+
/** git branch recorded by the agent, if any. */
|
|
35
|
+
gitBranch?: string;
|
|
36
|
+
/** Token usage so far (for cost-spike detection). */
|
|
37
|
+
tokens?: {
|
|
38
|
+
input: number;
|
|
39
|
+
output: number;
|
|
40
|
+
};
|
|
41
|
+
/** Tool name awaiting a permission decision, if the session is blocked on one. */
|
|
42
|
+
pendingPermission?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* One normalized session. The merge target for every adapter's output.
|
|
46
|
+
*/
|
|
47
|
+
export interface AgentSession {
|
|
48
|
+
agent: AgentKind;
|
|
49
|
+
/** Stable id within this agent (uuid, rollout filename, PR number, …). */
|
|
50
|
+
sessionId: string;
|
|
51
|
+
/** Human label — usually the basename of cwd. */
|
|
52
|
+
project: string;
|
|
53
|
+
cwd?: string;
|
|
54
|
+
state: AgentSessionState;
|
|
55
|
+
/** Short machine-ish reason for the state ("end_turn", "permission", …). */
|
|
56
|
+
stateReason: string;
|
|
57
|
+
/** Last activity time, epoch ms. */
|
|
58
|
+
lastMtime: number;
|
|
59
|
+
/** L2 activity, present when the adapter runs at tier ≥ "activity". */
|
|
60
|
+
activity?: SessionActivity;
|
|
61
|
+
}
|
|
62
|
+
/** Visibility tier — how deeply LISA may inspect a session. See plan §3. */
|
|
63
|
+
export type VisibilityTier = "off" | "metadata" | "activity" | "intent";
|
|
64
|
+
/** Per-integration config from ~/.lisa/agents.json. */
|
|
65
|
+
export interface AgentIntegrationConfig {
|
|
66
|
+
enabled?: boolean;
|
|
67
|
+
/** Home dir / data dir override (e.g. CODEX_HOME, OPENCODE_DATA_DIR). */
|
|
68
|
+
home?: string;
|
|
69
|
+
/** Extra roots to watch (e.g. Aider's per-project logs). */
|
|
70
|
+
watchRoots?: string[];
|
|
71
|
+
/** Per-integration visibility override; falls back to the global tier. */
|
|
72
|
+
visibility?: VisibilityTier;
|
|
73
|
+
/** Arbitrary adapter-specific options. */
|
|
74
|
+
[k: string]: unknown;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* An adapter that watches one agent kind and emits normalized sessions.
|
|
78
|
+
* Stateful: holds file watchers / timers between start() and stop().
|
|
79
|
+
*/
|
|
80
|
+
export interface AgentObserver {
|
|
81
|
+
readonly agent: AgentKind;
|
|
82
|
+
/** Begin watching. `emit` is called on every new/changed session. */
|
|
83
|
+
start(emit: (session: AgentSession) => void): Promise<void>;
|
|
84
|
+
/** Current snapshot of active sessions. */
|
|
85
|
+
list(): AgentSession[];
|
|
86
|
+
/** Tear down watchers/timers. Idempotent. */
|
|
87
|
+
stop(): Promise<void>;
|
|
88
|
+
}
|
|
89
|
+
export type AgentObserverFactory = (cfg: AgentIntegrationConfig) => AgentObserver | Promise<AgentObserver>;
|
|
90
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/integrations/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;gDACgD;AAChD,MAAM,MAAM,SAAS,GACjB,aAAa,GACb,OAAO,GACP,UAAU,GACV,OAAO,GACP,QAAQ,GACR,QAAQ,GACR,WAAW,GACX,KAAK,GACL,MAAM,GACN,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAElB;mEACmE;AACnE,MAAM,MAAM,iBAAiB,GACzB,SAAS,GACT,SAAS,GACT,OAAO,GACP,MAAM,GACN,MAAM,GACN,SAAS,CAAC;AAEd;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,sEAAsE;IACtE,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,mFAAmF;IACnF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mEAAmE;IACnE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qDAAqD;IACrD,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3C,kFAAkF;IAClF,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,SAAS,CAAC;IACjB,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAC;IAClB,iDAAiD;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,iBAAiB,CAAC;IACzB,4EAA4E;IAC5E,WAAW,EAAE,MAAM,CAAC;IACpB,oCAAoC;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,QAAQ,CAAC,EAAE,eAAe,CAAC;CAC5B;AAED,4EAA4E;AAC5E,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,UAAU,GAAG,UAAU,GAAG,QAAQ,CAAC;AAExE,uDAAuD;AACvD,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,yEAAyE;IACzE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,0CAA0C;IAC1C,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACtB;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,qEAAqE;IACrE,KAAK,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,2CAA2C;IAC3C,IAAI,IAAI,YAAY,EAAE,CAAC;IACvB,6CAA6C;IAC7C,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,MAAM,MAAM,oBAAoB,GAAG,CACjC,GAAG,EAAE,sBAAsB,KACxB,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-agent orchestration — shared types.
|
|
3
|
+
*
|
|
4
|
+
* LISA observes many CLI agents (Claude Code, Codex, OpenCode, Aider, …),
|
|
5
|
+
* each with its own on-disk session format. These types are the normalized,
|
|
6
|
+
* agent-agnostic shape every adapter produces, so the UI, the hub, and the
|
|
7
|
+
* advisor never care which agent a session came from.
|
|
8
|
+
*
|
|
9
|
+
* See docs/ORCHESTRATOR_PLAN.md for the full design (L1 OBSERVE, L2 UNDERSTAND).
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/integrations/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
|
package/dist/memory/vector.d.ts
CHANGED
|
@@ -12,7 +12,11 @@ interface Index {
|
|
|
12
12
|
docs: Document[];
|
|
13
13
|
idf: Map<string, number>;
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
/** Drop the cached index (test hook / explicit invalidation). */
|
|
16
|
+
export declare function clearIndexCache(): void;
|
|
17
|
+
export declare function buildIndex(opts?: {
|
|
18
|
+
cache?: boolean;
|
|
19
|
+
}): Promise<Index>;
|
|
16
20
|
export interface SearchHit {
|
|
17
21
|
sessionId: string;
|
|
18
22
|
startedAt: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vector.d.ts","sourceRoot":"","sources":["../../src/memory/vector.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"vector.d.ts","sourceRoot":"","sources":["../../src/memory/vector.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAKlC,UAAU,QAAQ;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACtB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AAED,UAAU,KAAK;IACb,IAAI,EAAE,QAAQ,EAAE,CAAC;IACjB,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC1B;AA4CD,iEAAiE;AACjE,wBAAgB,eAAe,IAAI,IAAI,CAGtC;AAED,wBAAsB,UAAU,CAAC,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CAW/E;AAsCD,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,SAAI,GAAG,SAAS,EAAE,CAuBtE;AA2BD,eAAO,MAAM,qBAAqB,EAAE,OAAO,IAAI,GAAG,OAAO,EAAE,GAAG,SAAqB,CAAC"}
|
package/dist/memory/vector.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
1
3
|
import { listSessionsOnDisk, loadSessionMessages } from "../sessions/list.js";
|
|
4
|
+
import { SESSIONS_DIR } from "../paths.js";
|
|
2
5
|
import { extractTextFromContent } from "../agent.js";
|
|
3
6
|
const STOPWORDS = new Set([
|
|
4
7
|
"the", "a", "an", "of", "to", "in", "and", "or", "for", "is", "it",
|
|
@@ -6,7 +9,61 @@ const STOPWORDS = new Set([
|
|
|
6
9
|
"by", "from", "but", "not", "i", "you", "we", "they", "he", "she",
|
|
7
10
|
"lisa", "tool", "use", "user", "assistant",
|
|
8
11
|
]);
|
|
9
|
-
|
|
12
|
+
// ── index cache ───────────────────────────────────────────────────────────
|
|
13
|
+
// buildIndex re-reads + re-tokenizes EVERY past session, which is O(total
|
|
14
|
+
// transcript bytes). memory_search was calling it fresh on every invocation,
|
|
15
|
+
// so a chat that searches its memory N times in one session paid that cost N
|
|
16
|
+
// times even when nothing changed. We cache the built index keyed by a cheap
|
|
17
|
+
// fingerprint of the sessions directory (each .jsonl's mtime + size). The
|
|
18
|
+
// fingerprint changes the instant any session is appended to or a new one
|
|
19
|
+
// appears, so the cache is correct, not just fast.
|
|
20
|
+
let cachedIndex = null;
|
|
21
|
+
let cachedFingerprint = "";
|
|
22
|
+
async function sessionsFingerprint() {
|
|
23
|
+
let files;
|
|
24
|
+
try {
|
|
25
|
+
files = await fs.readdir(SESSIONS_DIR);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Missing dir and empty dir must fingerprint identically: buildIndex →
|
|
29
|
+
// listSessionsOnDisk → ensureDir CREATES the dir as a side effect, so a
|
|
30
|
+
// distinct "no-dir" sentinel would spuriously miss the cache on the very
|
|
31
|
+
// next call. Both collapse to the empty fingerprint.
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
const parts = [];
|
|
35
|
+
for (const f of files.sort()) {
|
|
36
|
+
if (!f.endsWith(".jsonl"))
|
|
37
|
+
continue;
|
|
38
|
+
try {
|
|
39
|
+
const st = await fs.stat(path.join(SESSIONS_DIR, f));
|
|
40
|
+
parts.push(`${f}:${st.mtimeMs}:${st.size}`);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// file vanished between readdir and stat — ignore
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return parts.join("|");
|
|
47
|
+
}
|
|
48
|
+
/** Drop the cached index (test hook / explicit invalidation). */
|
|
49
|
+
export function clearIndexCache() {
|
|
50
|
+
cachedIndex = null;
|
|
51
|
+
cachedFingerprint = "";
|
|
52
|
+
}
|
|
53
|
+
export async function buildIndex(opts = {}) {
|
|
54
|
+
const useCache = opts.cache !== false;
|
|
55
|
+
if (useCache) {
|
|
56
|
+
const fp = await sessionsFingerprint();
|
|
57
|
+
if (cachedIndex && fp === cachedFingerprint)
|
|
58
|
+
return cachedIndex;
|
|
59
|
+
const built = await buildIndexUncached();
|
|
60
|
+
cachedIndex = built;
|
|
61
|
+
cachedFingerprint = fp;
|
|
62
|
+
return built;
|
|
63
|
+
}
|
|
64
|
+
return buildIndexUncached();
|
|
65
|
+
}
|
|
66
|
+
async function buildIndexUncached() {
|
|
10
67
|
const sessions = await listSessionsOnDisk();
|
|
11
68
|
const docs = [];
|
|
12
69
|
const docFreq = new Map();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vector.js","sourceRoot":"","sources":["../../src/memory/vector.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"vector.js","sourceRoot":"","sources":["../../src/memory/vector.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-process advisory file lock for soul writes.
|
|
3
|
+
*
|
|
4
|
+
* The in-process commit queue (git.ts enqueueCommit) serializes writes within
|
|
5
|
+
* ONE process, but LISA runs as several processes against the same
|
|
6
|
+
* ~/.lisa/soul/: the web server, the CLI/REPL, and — crucially — the
|
|
7
|
+
* heartbeat/idle runners launched by launchd/cron on their own clock. Two of
|
|
8
|
+
* those writing concurrently can:
|
|
9
|
+
* - lose a desire-progress append (read-modify-write race), or
|
|
10
|
+
* - collide on .git/index.lock (one commit fails).
|
|
11
|
+
*
|
|
12
|
+
* withFileLock implements a portable advisory mutex via exclusive file
|
|
13
|
+
* creation (`open(..., "wx")` succeeds only if the file doesn't exist). It
|
|
14
|
+
* self-heals from a crashed holder via a staleness timeout + a best-effort
|
|
15
|
+
* liveness check on the recorded pid.
|
|
16
|
+
*/
|
|
17
|
+
export interface FileLockOpts {
|
|
18
|
+
/** Treat a held lock as abandoned after this many ms (crashed holder). */
|
|
19
|
+
staleMs?: number;
|
|
20
|
+
/** Give up acquiring after this many ms. */
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
/** Poll interval while waiting for the lock. */
|
|
23
|
+
pollMs?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Run `fn` while holding an exclusive lock at `lockPath`. Releases the lock
|
|
27
|
+
* (deletes the file) when `fn` settles, success or failure.
|
|
28
|
+
*/
|
|
29
|
+
export declare function withFileLock<T>(lockPath: string, fn: () => Promise<T>, opts?: FileLockOpts): Promise<T>;
|
|
30
|
+
/** The canonical soul write-lock path. */
|
|
31
|
+
export declare const SOUL_LOCK_PATH: string;
|
|
32
|
+
/**
|
|
33
|
+
* Run `fn` while holding the soul write-lock. Use this around any
|
|
34
|
+
* read-modify-write of soul state (e.g. desire-progress appends) so a
|
|
35
|
+
* concurrent heartbeat/idle/chat process can't interleave and lose data.
|
|
36
|
+
*/
|
|
37
|
+
export declare function withSoulLock<T>(fn: () => Promise<T>, opts?: FileLockOpts): Promise<T>;
|
|
38
|
+
//# sourceMappingURL=lock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.d.ts","sourceRoot":"","sources":["../../src/soul/lock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAOH,MAAM,WAAW,YAAY;IAC3B,0EAA0E;IAC1E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4CAA4C;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gDAAgD;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAoCD;;;GAGG;AACH,wBAAsB,YAAY,CAAC,CAAC,EAClC,QAAQ,EAAE,MAAM,EAChB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,IAAI,GAAE,YAAiB,GACtB,OAAO,CAAC,CAAC,CAAC,CAmCZ;AAED,0CAA0C;AAC1C,eAAO,MAAM,cAAc,QAAqC,CAAC;AAEjE;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,CAAC,CAAC,CAErF"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-process advisory file lock for soul writes.
|
|
3
|
+
*
|
|
4
|
+
* The in-process commit queue (git.ts enqueueCommit) serializes writes within
|
|
5
|
+
* ONE process, but LISA runs as several processes against the same
|
|
6
|
+
* ~/.lisa/soul/: the web server, the CLI/REPL, and — crucially — the
|
|
7
|
+
* heartbeat/idle runners launched by launchd/cron on their own clock. Two of
|
|
8
|
+
* those writing concurrently can:
|
|
9
|
+
* - lose a desire-progress append (read-modify-write race), or
|
|
10
|
+
* - collide on .git/index.lock (one commit fails).
|
|
11
|
+
*
|
|
12
|
+
* withFileLock implements a portable advisory mutex via exclusive file
|
|
13
|
+
* creation (`open(..., "wx")` succeeds only if the file doesn't exist). It
|
|
14
|
+
* self-heals from a crashed holder via a staleness timeout + a best-effort
|
|
15
|
+
* liveness check on the recorded pid.
|
|
16
|
+
*/
|
|
17
|
+
import fsp from "node:fs/promises";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { ensureDir } from "../fs-utils.js";
|
|
20
|
+
import { SOUL_DIR } from "./paths.js";
|
|
21
|
+
const DEFAULTS = { staleMs: 30_000, timeoutMs: 10_000, pollMs: 50 };
|
|
22
|
+
function delay(ms) {
|
|
23
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
24
|
+
}
|
|
25
|
+
/** Is the process holding this lock gone or the lock too old to trust? */
|
|
26
|
+
async function isStale(lockPath, staleMs) {
|
|
27
|
+
let body;
|
|
28
|
+
try {
|
|
29
|
+
body = JSON.parse(await fsp.readFile(lockPath, "utf8"));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Unreadable / malformed lock file → treat as stale so we can recover.
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
if (typeof body.ts !== "number" || Date.now() - body.ts > staleMs)
|
|
36
|
+
return true;
|
|
37
|
+
// Best-effort liveness: signal 0 throws ESRCH if the pid is dead. Only
|
|
38
|
+
// meaningful on the same host; on a different host kill() may report the
|
|
39
|
+
// wrong answer, so we still rely primarily on the ts timeout above.
|
|
40
|
+
if (typeof body.pid === "number" && body.pid > 0) {
|
|
41
|
+
try {
|
|
42
|
+
process.kill(body.pid, 0);
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
if (e.code === "ESRCH")
|
|
46
|
+
return true; // holder dead
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Run `fn` while holding an exclusive lock at `lockPath`. Releases the lock
|
|
53
|
+
* (deletes the file) when `fn` settles, success or failure.
|
|
54
|
+
*/
|
|
55
|
+
export async function withFileLock(lockPath, fn, opts = {}) {
|
|
56
|
+
const { staleMs, timeoutMs, pollMs } = { ...DEFAULTS, ...opts };
|
|
57
|
+
await ensureDir(path.dirname(lockPath));
|
|
58
|
+
const deadline = Date.now() + timeoutMs;
|
|
59
|
+
// ── acquire ──
|
|
60
|
+
for (;;) {
|
|
61
|
+
try {
|
|
62
|
+
const fh = await fsp.open(lockPath, "wx");
|
|
63
|
+
try {
|
|
64
|
+
await fh.writeFile(JSON.stringify({ pid: process.pid, ts: Date.now() }));
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
await fh.close();
|
|
68
|
+
}
|
|
69
|
+
break; // acquired
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
if (e.code !== "EEXIST")
|
|
73
|
+
throw e;
|
|
74
|
+
// Held by someone. Steal if stale, else wait.
|
|
75
|
+
if (await isStale(lockPath, staleMs)) {
|
|
76
|
+
await fsp.rm(lockPath, { force: true }).catch(() => { });
|
|
77
|
+
continue; // retry immediately
|
|
78
|
+
}
|
|
79
|
+
if (Date.now() >= deadline) {
|
|
80
|
+
throw new Error(`timed out acquiring lock ${lockPath} after ${timeoutMs}ms`);
|
|
81
|
+
}
|
|
82
|
+
await delay(pollMs);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// ── critical section ──
|
|
86
|
+
try {
|
|
87
|
+
return await fn();
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
await fsp.rm(lockPath, { force: true }).catch(() => { });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** The canonical soul write-lock path. */
|
|
94
|
+
export const SOUL_LOCK_PATH = path.join(SOUL_DIR, ".write.lock");
|
|
95
|
+
/**
|
|
96
|
+
* Run `fn` while holding the soul write-lock. Use this around any
|
|
97
|
+
* read-modify-write of soul state (e.g. desire-progress appends) so a
|
|
98
|
+
* concurrent heartbeat/idle/chat process can't interleave and lose data.
|
|
99
|
+
*/
|
|
100
|
+
export function withSoulLock(fn, opts) {
|
|
101
|
+
return withFileLock(SOUL_LOCK_PATH, fn, opts);
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.js","sourceRoot":"","sources":["../../src/soul/lock.ts"],"names":[],"mappings":"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"}
|
package/dist/soul/paths.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/soul/paths.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/soul/paths.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,QAAQ,QAA+B,CAAC;AAErD,eAAO,MAAM,SAAS,QAAmC,CAAC;AAC1D,eAAO,MAAM,SAAS,QAAiC,CAAC;AACxD,eAAO,MAAM,aAAa,QAAqC,CAAC;AAChE,eAAO,MAAM,YAAY,QAAoC,CAAC;AAC9D,eAAO,MAAM,iBAAiB,QAAyC,CAAC;AACxE,eAAO,MAAM,aAAa,QAAuC,CAAC;AAClE,eAAO,MAAM,SAAS,QAAwC,CAAC;AAE/D,eAAO,MAAM,eAAe,QAAgC,CAAC;AAC7D,eAAO,MAAM,iBAAiB,QAAkC,CAAC;AACjE,eAAO,MAAM,gBAAgB,QAAiC,CAAC;AAC/D,eAAO,MAAM,gBAAgB,QAAiC,CAAC;AAC/D,eAAO,MAAM,sBAAsB,QAAuC,CAAC;AAK3E,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AACD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AACD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AACD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvD;AACD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAIhD;AACD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAExD"}
|
package/dist/soul/paths.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { LISA_HOME } from "../paths.js";
|
|
3
|
+
import { assertSafeSlug } from "./slug.js";
|
|
3
4
|
export const SOUL_DIR = path.join(LISA_HOME, "soul");
|
|
4
5
|
export const SOUL_SEED = path.join(SOUL_DIR, "seed.json");
|
|
5
6
|
export const SOUL_NAME = path.join(SOUL_DIR, "name.md");
|
|
@@ -13,22 +14,27 @@ export const SOUL_OPINIONS_DIR = path.join(SOUL_DIR, "opinions");
|
|
|
13
14
|
export const SOUL_DESIRES_DIR = path.join(SOUL_DIR, "desires");
|
|
14
15
|
export const SOUL_JOURNAL_DIR = path.join(SOUL_DIR, "journal");
|
|
15
16
|
export const SOUL_RELATIONSHIPS_DIR = path.join(SOUL_DIR, "relationships");
|
|
17
|
+
// Every slug-bearing path helper runs the slug through assertSafeSlug first.
|
|
18
|
+
// This is the single chokepoint for path-traversal defense: a slug like
|
|
19
|
+
// "../../../etc/x" or "a/b" throws here rather than escaping the soul dir.
|
|
16
20
|
export function valueFile(slug) {
|
|
17
|
-
return path.join(SOUL_VALUES_DIR, `${slug}.md`);
|
|
21
|
+
return path.join(SOUL_VALUES_DIR, `${assertSafeSlug(slug)}.md`);
|
|
18
22
|
}
|
|
19
23
|
export function opinionFile(slug) {
|
|
20
|
-
return path.join(SOUL_OPINIONS_DIR, `${slug}.md`);
|
|
24
|
+
return path.join(SOUL_OPINIONS_DIR, `${assertSafeSlug(slug)}.md`);
|
|
21
25
|
}
|
|
22
26
|
export function desireFile(slug) {
|
|
23
|
-
return path.join(SOUL_DESIRES_DIR, `${slug}.md`);
|
|
27
|
+
return path.join(SOUL_DESIRES_DIR, `${assertSafeSlug(slug)}.md`);
|
|
24
28
|
}
|
|
25
29
|
export function desireProgressFile(slug) {
|
|
26
|
-
return path.join(SOUL_DESIRES_DIR, `${slug}.progress.md`);
|
|
30
|
+
return path.join(SOUL_DESIRES_DIR, `${assertSafeSlug(slug)}.progress.md`);
|
|
27
31
|
}
|
|
28
32
|
export function journalFile(date) {
|
|
29
|
-
|
|
33
|
+
// Journal keys are dates (YYYY-MM-DD), which assertSafeSlug accepts (no
|
|
34
|
+
// separators/dots-leading), so this guards against a malformed date too.
|
|
35
|
+
return path.join(SOUL_JOURNAL_DIR, `${assertSafeSlug(date)}.md`);
|
|
30
36
|
}
|
|
31
37
|
export function relationshipFile(userKey) {
|
|
32
|
-
return path.join(SOUL_RELATIONSHIPS_DIR, `${userKey}.md`);
|
|
38
|
+
return path.join(SOUL_RELATIONSHIPS_DIR, `${assertSafeSlug(userKey)}.md`);
|
|
33
39
|
}
|
|
34
40
|
//# sourceMappingURL=paths.js.map
|
package/dist/soul/paths.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/soul/paths.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/soul/paths.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAE3C,MAAM,CAAC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;AAErD,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;AAC1D,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;AACxD,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;AAChE,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;AAC9D,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,iBAAiB,CAAC,CAAC;AACxE,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAC;AAClE,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC;AAE/D,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AAC7D,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;AACjE,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;AAC/D,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;AAC/D,MAAM,CAAC,MAAM,sBAAsB,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAC;AAE3E,6EAA6E;AAC7E,wEAAwE;AACxE,2EAA2E;AAC3E,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,OAAO,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,GAAG,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAClE,CAAC;AACD,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,GAAG,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACpE,CAAC;AACD,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,GAAG,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACnE,CAAC;AACD,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,OAAO,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,GAAG,cAAc,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;AAC5E,CAAC;AACD,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,wEAAwE;IACxE,yEAAyE;IACzE,OAAO,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,GAAG,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACnE,CAAC;AACD,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC9C,OAAO,IAAI,CAAC,IAAI,CAAC,sBAAsB,EAAE,GAAG,cAAc,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC5E,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slug safety for soul file paths.
|
|
3
|
+
*
|
|
4
|
+
* Soul state lives one-file-per-entry under ~/.lisa/soul/{values,opinions,
|
|
5
|
+
* desires,relationships}/<slug>.md. The slug comes from the LLM (via
|
|
6
|
+
* soul_patch / desire tools) or from reflect operations, so it is untrusted
|
|
7
|
+
* input that gets joined into a filesystem path. Without validation, a slug
|
|
8
|
+
* like "../../../etc/something" or "foo/bar" would let a write escape the
|
|
9
|
+
* soul directory (path traversal) or silently nest into subdirectories.
|
|
10
|
+
*
|
|
11
|
+
* Two functions, two jobs:
|
|
12
|
+
* - assertSafeSlug — a HARD GATE applied in every path helper (read AND
|
|
13
|
+
* write). Throws on anything that could traverse or escape. Deliberately
|
|
14
|
+
* permissive about case/style so it never breaks reads of slugs written
|
|
15
|
+
* by older versions; it only blocks genuinely dangerous shapes.
|
|
16
|
+
* - normalizeSlug — a SOFT CLEANER applied when a NEW slug is first minted
|
|
17
|
+
* from free-form input. Lowercases, strips to [a-z0-9-], collapses dashes,
|
|
18
|
+
* caps length — produces a tidy, collision-resistant slug.
|
|
19
|
+
*/
|
|
20
|
+
export declare const MAX_SLUG_LEN = 64;
|
|
21
|
+
export declare class UnsafeSlugError extends Error {
|
|
22
|
+
constructor(slug: string, reason: string);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Throw if `slug` could escape its soul subdirectory or corrupt the path.
|
|
26
|
+
* This is the security boundary — applied in paths.ts so no code path can
|
|
27
|
+
* construct a soul file path from a dangerous slug.
|
|
28
|
+
*/
|
|
29
|
+
export declare function assertSafeSlug(slug: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Produce a tidy slug from free-form text. Use when minting a NEW slug.
|
|
32
|
+
* - lowercases
|
|
33
|
+
* - replaces any run of non [a-z0-9] with a single dash
|
|
34
|
+
* - trims leading/trailing dashes
|
|
35
|
+
* - caps length
|
|
36
|
+
* Returns "" only if the input had no usable characters; callers should
|
|
37
|
+
* treat "" as "generate a fallback" (e.g. a timestamp-based slug).
|
|
38
|
+
*/
|
|
39
|
+
export declare function normalizeSlug(raw: string): string;
|
|
40
|
+
//# sourceMappingURL=slug.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slug.d.ts","sourceRoot":"","sources":["../../src/soul/slug.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,eAAO,MAAM,YAAY,KAAK,CAAC;AAE/B,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAIzC;AAWD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAmBnD;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAOjD"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slug safety for soul file paths.
|
|
3
|
+
*
|
|
4
|
+
* Soul state lives one-file-per-entry under ~/.lisa/soul/{values,opinions,
|
|
5
|
+
* desires,relationships}/<slug>.md. The slug comes from the LLM (via
|
|
6
|
+
* soul_patch / desire tools) or from reflect operations, so it is untrusted
|
|
7
|
+
* input that gets joined into a filesystem path. Without validation, a slug
|
|
8
|
+
* like "../../../etc/something" or "foo/bar" would let a write escape the
|
|
9
|
+
* soul directory (path traversal) or silently nest into subdirectories.
|
|
10
|
+
*
|
|
11
|
+
* Two functions, two jobs:
|
|
12
|
+
* - assertSafeSlug — a HARD GATE applied in every path helper (read AND
|
|
13
|
+
* write). Throws on anything that could traverse or escape. Deliberately
|
|
14
|
+
* permissive about case/style so it never breaks reads of slugs written
|
|
15
|
+
* by older versions; it only blocks genuinely dangerous shapes.
|
|
16
|
+
* - normalizeSlug — a SOFT CLEANER applied when a NEW slug is first minted
|
|
17
|
+
* from free-form input. Lowercases, strips to [a-z0-9-], collapses dashes,
|
|
18
|
+
* caps length — produces a tidy, collision-resistant slug.
|
|
19
|
+
*/
|
|
20
|
+
export const MAX_SLUG_LEN = 64;
|
|
21
|
+
export class UnsafeSlugError extends Error {
|
|
22
|
+
constructor(slug, reason) {
|
|
23
|
+
super(`unsafe soul slug ${JSON.stringify(slug)}: ${reason}`);
|
|
24
|
+
this.name = "UnsafeSlugError";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Control characters (codepoints 0x00–0x1f): newlines, tabs, null, etc.
|
|
28
|
+
// Checked via codepoint so the source stays plain-ASCII.
|
|
29
|
+
function hasControlChar(s) {
|
|
30
|
+
for (let i = 0; i < s.length; i++) {
|
|
31
|
+
if (s.charCodeAt(i) < 0x20)
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Throw if `slug` could escape its soul subdirectory or corrupt the path.
|
|
38
|
+
* This is the security boundary — applied in paths.ts so no code path can
|
|
39
|
+
* construct a soul file path from a dangerous slug.
|
|
40
|
+
*/
|
|
41
|
+
export function assertSafeSlug(slug) {
|
|
42
|
+
if (typeof slug !== "string" || slug.length === 0) {
|
|
43
|
+
throw new UnsafeSlugError(String(slug), "empty");
|
|
44
|
+
}
|
|
45
|
+
if (slug.length > MAX_SLUG_LEN) {
|
|
46
|
+
throw new UnsafeSlugError(slug, `longer than ${MAX_SLUG_LEN} chars`);
|
|
47
|
+
}
|
|
48
|
+
if (hasControlChar(slug)) {
|
|
49
|
+
throw new UnsafeSlugError(slug, "contains control characters");
|
|
50
|
+
}
|
|
51
|
+
if (slug.includes("/") || slug.includes("\\")) {
|
|
52
|
+
throw new UnsafeSlugError(slug, "contains a path separator");
|
|
53
|
+
}
|
|
54
|
+
// Block "." / ".." and any leading-dot form — path-traversal primitives
|
|
55
|
+
// and dotfiles that would hide from directory listings.
|
|
56
|
+
if (slug.startsWith(".")) {
|
|
57
|
+
throw new UnsafeSlugError(slug, "starts with a dot");
|
|
58
|
+
}
|
|
59
|
+
return slug;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Produce a tidy slug from free-form text. Use when minting a NEW slug.
|
|
63
|
+
* - lowercases
|
|
64
|
+
* - replaces any run of non [a-z0-9] with a single dash
|
|
65
|
+
* - trims leading/trailing dashes
|
|
66
|
+
* - caps length
|
|
67
|
+
* Returns "" only if the input had no usable characters; callers should
|
|
68
|
+
* treat "" as "generate a fallback" (e.g. a timestamp-based slug).
|
|
69
|
+
*/
|
|
70
|
+
export function normalizeSlug(raw) {
|
|
71
|
+
return raw
|
|
72
|
+
.toLowerCase()
|
|
73
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
74
|
+
.replace(/^-+|-+$/g, "")
|
|
75
|
+
.slice(0, MAX_SLUG_LEN)
|
|
76
|
+
.replace(/-+$/g, ""); // re-trim in case slice landed mid-dash-run
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=slug.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slug.js","sourceRoot":"","sources":["../../src/soul/slug.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,CAAC,MAAM,YAAY,GAAG,EAAE,CAAC;AAE/B,MAAM,OAAO,eAAgB,SAAQ,KAAK;IACxC,YAAY,IAAY,EAAE,MAAc;QACtC,KAAK,CAAC,oBAAoB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,MAAM,EAAE,CAAC,CAAC;QAC7D,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAChC,CAAC;CACF;AAED,wEAAwE;AACxE,yDAAyD;AACzD,SAAS,cAAc,CAAC,CAAS;IAC/B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAClC,IAAI,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI;YAAE,OAAO,IAAI,CAAC;IAC1C,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,eAAe,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC;IACnD,CAAC;IACD,IAAI,IAAI,CAAC,MAAM,GAAG,YAAY,EAAE,CAAC;QAC/B,MAAM,IAAI,eAAe,CAAC,IAAI,EAAE,eAAe,YAAY,QAAQ,CAAC,CAAC;IACvE,CAAC;IACD,IAAI,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,eAAe,CAAC,IAAI,EAAE,6BAA6B,CAAC,CAAC;IACjE,CAAC;IACD,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9C,MAAM,IAAI,eAAe,CAAC,IAAI,EAAE,2BAA2B,CAAC,CAAC;IAC/D,CAAC;IACD,wEAAwE;IACxE,wDAAwD;IACxD,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,eAAe,CAAC,IAAI,EAAE,mBAAmB,CAAC,CAAC;IACvD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,OAAO,GAAG;SACP,WAAW,EAAE;SACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;SACvB,KAAK,CAAC,CAAC,EAAE,YAAY,CAAC;SACtB,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,4CAA4C;AACtE,CAAC"}
|
package/dist/soul/store.d.ts
CHANGED
|
@@ -13,7 +13,18 @@ export declare function readConstitution(): Promise<string>;
|
|
|
13
13
|
export declare function writeConstitution(text: string): Promise<void>;
|
|
14
14
|
export declare function readEmotions(): Promise<EmotionState>;
|
|
15
15
|
export declare function writeEmotions(state: EmotionState): Promise<void>;
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Apply exponential decay to all emotion intensities based on elapsed time
|
|
18
|
+
* since `state.updatedAt`. Pure modulo the clock: pass `nowMs` to make it
|
|
19
|
+
* deterministic (tests / replay). Preserves the `events` trail and the
|
|
20
|
+
* per-emotion `decay` rates — only the `values` and `updatedAt` change.
|
|
21
|
+
*
|
|
22
|
+
* Called BOTH on read (display) and at the start of soul_feel (so the
|
|
23
|
+
* persisted baseline is always decay-correct before a new delta is added).
|
|
24
|
+
* Previously decay only ran on read, so soul_feel added deltas onto a stale
|
|
25
|
+
* (un-decayed) value and the stored intensity could be months out of date.
|
|
26
|
+
*/
|
|
27
|
+
export declare function decayEmotions(state: EmotionState, nowMs?: number): EmotionState;
|
|
17
28
|
export declare function listValues(): Promise<ValueEntry[]>;
|
|
18
29
|
export declare function writeValue(entry: ValueEntry): Promise<void>;
|
|
19
30
|
export declare function listOpinions(): Promise<OpinionEntry[]>;
|