@siddicky/oh-my-musecode 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/.claude-plugin/marketplace.json +18 -0
- package/.claude-plugin/plugin.json +30 -0
- package/.muse-plugin/plugin.json +94 -0
- package/LICENSE +32 -0
- package/README.md +190 -0
- package/dist/mcp/state-server.d.ts +13 -0
- package/dist/mcp/state-server.js +109 -0
- package/dist/mcp/state-server.js.map +1 -0
- package/dist/paths.d.ts +58 -0
- package/dist/paths.js +167 -0
- package/dist/paths.js.map +1 -0
- package/dist/personas.d.ts +40 -0
- package/dist/personas.js +93 -0
- package/dist/personas.js.map +1 -0
- package/dist/state.d.ts +38 -0
- package/dist/state.js +59 -0
- package/dist/state.js.map +1 -0
- package/docs/recipe.md +253 -0
- package/hooks/hooks.json +34 -0
- package/hooks/lib.mjs +66 -0
- package/hooks/routing.mjs +99 -0
- package/hooks/session-start.mjs +34 -0
- package/hooks/stop.mjs +52 -0
- package/hooks/user-prompt-submit.mjs +16 -0
- package/package.json +57 -0
- package/personas/architect/SOUL.md +27 -0
- package/personas/code-reviewer/SOUL.md +30 -0
- package/personas/critic/SOUL.md +28 -0
- package/personas/debugger/SOUL.md +28 -0
- package/personas/executor/SOUL.md +25 -0
- package/personas/explore/SOUL.md +24 -0
- package/personas/manifest.json +119 -0
- package/personas/planner/SOUL.md +25 -0
- package/personas/test-engineer/SOUL.md +27 -0
- package/personas/verifier/SOUL.md +29 -0
- package/personas/writer/SOUL.md +27 -0
- package/scripts/install.mjs +303 -0
- package/scripts/preflight.mjs +121 -0
- package/scripts/settings-install.mjs +155 -0
- package/scripts/verify-manifest.mjs +211 -0
- package/scripts/verify-skills.mjs +78 -0
- package/skills/cancel/SKILL.md +76 -0
- package/skills/deep-dive/SKILL.md +73 -0
- package/skills/deep-interview/SKILL.md +101 -0
- package/skills/ralph/SKILL.md +111 -0
- package/skills/ralplan/SKILL.md +96 -0
- package/skills/team/SKILL.md +94 -0
- package/skills/trace/SKILL.md +75 -0
package/dist/paths.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State-root resolution and protected-path enforcement.
|
|
3
|
+
*
|
|
4
|
+
* muse protects `.agents/` and `.muse/` with two independent layers: a mediated
|
|
5
|
+
* `edit_file` write is held for human review with no standing grant, and a shell
|
|
6
|
+
* write fails read-only at the sandbox. Runtime state therefore cannot live in
|
|
7
|
+
* either, so oh-my-musecode keeps its own root at `.omm/`.
|
|
8
|
+
*
|
|
9
|
+
* This module is the single place that decides whether a path is writable, so the
|
|
10
|
+
* hooks and the MCP state server cannot drift apart on it.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, lstatSync, mkdirSync, realpathSync } from 'node:fs';
|
|
13
|
+
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
14
|
+
/** Directory name of the oh-my-musecode state root, relative to the workspace. */
|
|
15
|
+
export const STATE_ROOT_DIRNAME = '.omm';
|
|
16
|
+
/**
|
|
17
|
+
* Path prefixes muse protects. A write that resolves inside any of these is
|
|
18
|
+
* refused before it reaches the filesystem, so the failure is a clear error
|
|
19
|
+
* rather than an opaque read-only sandbox rejection mid-run.
|
|
20
|
+
*/
|
|
21
|
+
export const PROTECTED_DIRNAMES = Object.freeze(['.agents', '.muse', '.git']);
|
|
22
|
+
export class ProtectedPathError extends Error {
|
|
23
|
+
path;
|
|
24
|
+
protectedSegment;
|
|
25
|
+
constructor(path, protectedSegment) {
|
|
26
|
+
super(`Refusing to write ${path}: \`${protectedSegment}/\` is a muse-protected path. ` +
|
|
27
|
+
`State belongs under ${STATE_ROOT_DIRNAME}/.`);
|
|
28
|
+
this.name = 'ProtectedPathError';
|
|
29
|
+
this.path = path;
|
|
30
|
+
this.protectedSegment = protectedSegment;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export class EscapedStateRootError extends Error {
|
|
34
|
+
path;
|
|
35
|
+
constructor(path, stateRoot) {
|
|
36
|
+
super(`Refusing to write ${path}: resolved outside the state root ${stateRoot}.`);
|
|
37
|
+
this.name = 'EscapedStateRootError';
|
|
38
|
+
this.path = path;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Absolute path of the state root for a workspace. */
|
|
42
|
+
export function stateRoot(workspaceRoot) {
|
|
43
|
+
return join(resolve(workspaceRoot), STATE_ROOT_DIRNAME);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Creates the state root if absent. Returns its absolute path.
|
|
47
|
+
*
|
|
48
|
+
* Called by the SessionStart hook. Deliberately does not touch `.agents/` or
|
|
49
|
+
* `.muse/`, which the harness owns.
|
|
50
|
+
*/
|
|
51
|
+
export function ensureStateRoot(workspaceRoot) {
|
|
52
|
+
const root = stateRoot(workspaceRoot);
|
|
53
|
+
if (!existsSync(root)) {
|
|
54
|
+
mkdirSync(join(root, 'state'), { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
return root;
|
|
57
|
+
}
|
|
58
|
+
/** True when `candidate` is inside `parent` (or is `parent` itself). */
|
|
59
|
+
function isInside(parent, candidate) {
|
|
60
|
+
const rel = relative(parent, candidate);
|
|
61
|
+
// `rel.startsWith('..')` alone would also reject a legitimate in-root name like
|
|
62
|
+
// `..notes`, so require the `..` to be a whole segment.
|
|
63
|
+
return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
|
|
64
|
+
}
|
|
65
|
+
export class SymlinkedStateRootError extends Error {
|
|
66
|
+
path;
|
|
67
|
+
constructor(root) {
|
|
68
|
+
super(`Refusing to use ${root}: the state root itself is a symbolic link. ` +
|
|
69
|
+
`Resolving it would relocate every state operation to its target, which is ` +
|
|
70
|
+
`exactly the escape the traversal check exists to prevent.`);
|
|
71
|
+
this.name = 'SymlinkedStateRootError';
|
|
72
|
+
this.path = root;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export class SymlinkTraversalError extends Error {
|
|
76
|
+
path;
|
|
77
|
+
constructor(path, linkPath) {
|
|
78
|
+
super(`Refusing to write ${path}: the path crosses a symbolic link (${linkPath}). ` +
|
|
79
|
+
`Symlinks can point outside the state root, so they are not followed.`);
|
|
80
|
+
this.name = 'SymlinkTraversalError';
|
|
81
|
+
this.path = path;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Refuses any path that traverses a symbolic link between the state root and the
|
|
86
|
+
* target.
|
|
87
|
+
*
|
|
88
|
+
* Lexical containment is not sufficient on its own: `.omm/link -> /etc` passes
|
|
89
|
+
* every string check while resolving outside the root. Walking the existing
|
|
90
|
+
* prefix and rejecting symlinks closes that escape for both the final component
|
|
91
|
+
* and every intermediate directory.
|
|
92
|
+
*
|
|
93
|
+
* A residual TOCTOU window remains — the path is checked, then opened — because
|
|
94
|
+
* Node exposes no `openat`-style confined traversal. Narrowing it further would
|
|
95
|
+
* need a native binding; callers should not treat this as a boundary against a
|
|
96
|
+
* local attacker who can create symlinks inside `.omm/` concurrently.
|
|
97
|
+
*/
|
|
98
|
+
function assertNoSymlinkTraversal(root, resolved) {
|
|
99
|
+
const rel = relative(root, resolved);
|
|
100
|
+
if (rel === '')
|
|
101
|
+
return;
|
|
102
|
+
let current = root;
|
|
103
|
+
for (const segment of rel.split(sep)) {
|
|
104
|
+
current = join(current, segment);
|
|
105
|
+
let stats;
|
|
106
|
+
try {
|
|
107
|
+
stats = lstatSync(current);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// Does not exist yet: nothing below it can be a link either.
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (stats.isSymbolicLink()) {
|
|
114
|
+
throw new SymlinkTraversalError(resolved, current);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Resolves a caller-supplied path against the state root and refuses anything
|
|
120
|
+
* that escapes it or lands in a muse-protected directory.
|
|
121
|
+
*
|
|
122
|
+
* Both checks are needed and neither subsumes the other: the protected-path check
|
|
123
|
+
* gives an accurate message for the common mistake (`../../.agents/AGENTS.md`),
|
|
124
|
+
* while the containment check is the actual security boundary and catches every
|
|
125
|
+
* other escape.
|
|
126
|
+
*
|
|
127
|
+
* @throws {ProtectedPathError} when the path resolves into `.agents/`, `.muse/` or `.git/`
|
|
128
|
+
* @throws {EscapedStateRootError} when the path resolves outside the state root
|
|
129
|
+
*/
|
|
130
|
+
export function resolveWritablePath(workspaceRoot, requestedPath) {
|
|
131
|
+
const workspace = resolve(workspaceRoot);
|
|
132
|
+
const root = stateRoot(workspace);
|
|
133
|
+
const resolved = isAbsolute(requestedPath)
|
|
134
|
+
? resolve(requestedPath)
|
|
135
|
+
: resolve(root, requestedPath);
|
|
136
|
+
// Report a protected hit specifically, even when the path also escaped the
|
|
137
|
+
// state root — `../../.agents/AGENTS.md` is a more useful error than "escaped".
|
|
138
|
+
const segments = relative(workspace, resolved).split(sep);
|
|
139
|
+
for (const protectedName of PROTECTED_DIRNAMES) {
|
|
140
|
+
if (segments.includes(protectedName)) {
|
|
141
|
+
throw new ProtectedPathError(requestedPath, protectedName);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (!isInside(root, resolved)) {
|
|
145
|
+
throw new EscapedStateRootError(requestedPath, root);
|
|
146
|
+
}
|
|
147
|
+
// Lexical containment passed; now make sure the path does not reach outside the
|
|
148
|
+
// root through a link.
|
|
149
|
+
//
|
|
150
|
+
// The root itself is checked FIRST and separately. Resolving it with realpath
|
|
151
|
+
// before checking would silently relocate every subsequent operation: with
|
|
152
|
+
// `.omm -> /somewhere/else`, the traversal walk starts inside the target and
|
|
153
|
+
// finds nothing wrong, handing out a complete external read/write/delete
|
|
154
|
+
// primitive. The root link must be refused, not followed.
|
|
155
|
+
if (existsSync(root)) {
|
|
156
|
+
if (lstatSync(root).isSymbolicLink()) {
|
|
157
|
+
throw new SymlinkedStateRootError(root);
|
|
158
|
+
}
|
|
159
|
+
// Safe now that the root is known to be a real directory: realpath only
|
|
160
|
+
// normalises symlinked ancestors of the workspace (on macOS /tmp is a link
|
|
161
|
+
// to /private/tmp), so containment comparisons still line up.
|
|
162
|
+
const realRoot = realpathSync(root);
|
|
163
|
+
assertNoSymlinkTraversal(realRoot, resolve(realRoot, relative(root, resolved)));
|
|
164
|
+
}
|
|
165
|
+
return resolved;
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persona loading and prompt rendering.
|
|
3
|
+
*
|
|
4
|
+
* muse 1.0.3 rejects `agents` as a plugin capability entirely: a Claude-family
|
|
5
|
+
* plugin that declares it still loads, but the definitions are reported
|
|
6
|
+
* `agent-overlay-inactive` and never activate (see scripts/verify-manifest.mjs).
|
|
7
|
+
* So personas here are not muse agent definitions — they are plain data (a SOUL.md
|
|
8
|
+
* body plus a routing description and a narrowed toolset) that skills read and
|
|
9
|
+
* interpolate into a `subagent_spawn(role, objective)` prompt at call time. This
|
|
10
|
+
* module is the single place that resolves that data off disk.
|
|
11
|
+
*
|
|
12
|
+
* Each persona is a SOUL: the agent's "who," never the project's "what." Workspace
|
|
13
|
+
* paths, stack choices, and handoff formats belong in AGENTS.md / a runbook, not here.
|
|
14
|
+
*/
|
|
15
|
+
export interface Persona {
|
|
16
|
+
/** Matches the persona's directory name under personas/. */
|
|
17
|
+
readonly id: string;
|
|
18
|
+
/** Routing surface: what another agent reads to decide whether this persona fits. */
|
|
19
|
+
readonly description: string;
|
|
20
|
+
/** Narrowed toolset. Never widens whatever the spawning context already grants. */
|
|
21
|
+
readonly tools: readonly string[];
|
|
22
|
+
/** Full SOUL.md body: the persona's "who". */
|
|
23
|
+
readonly soul: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Loads every persona declared in personas/manifest.json, reading its SOUL.md
|
|
27
|
+
* from disk. Throws if the manifest and the SOUL.md files on disk disagree, or
|
|
28
|
+
* if any persona's `tools` array is empty (muse semantics: a child's tools may
|
|
29
|
+
* only narrow the inherited grant, never widen it — an empty toolset is a
|
|
30
|
+
* persona that can be spawned but can do nothing, which is never intended here).
|
|
31
|
+
*/
|
|
32
|
+
export declare function loadPersonas(): Persona[];
|
|
33
|
+
/**
|
|
34
|
+
* Renders a persona's prompt for interpolation into a `subagent_spawn(role,
|
|
35
|
+
* objective)` call: the SOUL body, followed by its routing description as
|
|
36
|
+
* additional framing context.
|
|
37
|
+
*
|
|
38
|
+
* @throws {Error} naming the valid ids, if `id` does not match a loaded persona
|
|
39
|
+
*/
|
|
40
|
+
export declare function renderPersonaPrompt(id: string): string;
|
package/dist/personas.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persona loading and prompt rendering.
|
|
3
|
+
*
|
|
4
|
+
* muse 1.0.3 rejects `agents` as a plugin capability entirely: a Claude-family
|
|
5
|
+
* plugin that declares it still loads, but the definitions are reported
|
|
6
|
+
* `agent-overlay-inactive` and never activate (see scripts/verify-manifest.mjs).
|
|
7
|
+
* So personas here are not muse agent definitions — they are plain data (a SOUL.md
|
|
8
|
+
* body plus a routing description and a narrowed toolset) that skills read and
|
|
9
|
+
* interpolate into a `subagent_spawn(role, objective)` prompt at call time. This
|
|
10
|
+
* module is the single place that resolves that data off disk.
|
|
11
|
+
*
|
|
12
|
+
* Each persona is a SOUL: the agent's "who," never the project's "what." Workspace
|
|
13
|
+
* paths, stack choices, and handoff formats belong in AGENTS.md / a runbook, not here.
|
|
14
|
+
*/
|
|
15
|
+
import { readFileSync } from 'node:fs';
|
|
16
|
+
import { dirname, join } from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
/**
|
|
20
|
+
* Personas directory, resolved relative to this module rather than the process
|
|
21
|
+
* cwd, so loading works the same whether this runs from src/ under ts-node or
|
|
22
|
+
* from the compiled dist/ output.
|
|
23
|
+
*/
|
|
24
|
+
const PERSONAS_DIR = join(MODULE_DIR, '..', 'personas');
|
|
25
|
+
function readManifest() {
|
|
26
|
+
const manifestPath = join(PERSONAS_DIR, 'manifest.json');
|
|
27
|
+
const raw = readFileSync(manifestPath, 'utf8');
|
|
28
|
+
return JSON.parse(raw);
|
|
29
|
+
}
|
|
30
|
+
function readSoul(id) {
|
|
31
|
+
const soulPath = join(PERSONAS_DIR, id, 'SOUL.md');
|
|
32
|
+
try {
|
|
33
|
+
return readFileSync(soulPath, 'utf8').trim();
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
throw new Error(`Persona "${id}" is listed in manifest.json but has no SOUL.md at ${soulPath}.`, { cause: err });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
let cachedPersonas;
|
|
40
|
+
/**
|
|
41
|
+
* Loads every persona declared in personas/manifest.json, reading its SOUL.md
|
|
42
|
+
* from disk. Throws if the manifest and the SOUL.md files on disk disagree, or
|
|
43
|
+
* if any persona's `tools` array is empty (muse semantics: a child's tools may
|
|
44
|
+
* only narrow the inherited grant, never widen it — an empty toolset is a
|
|
45
|
+
* persona that can be spawned but can do nothing, which is never intended here).
|
|
46
|
+
*/
|
|
47
|
+
export function loadPersonas() {
|
|
48
|
+
if (cachedPersonas)
|
|
49
|
+
return cachedPersonas;
|
|
50
|
+
const manifest = readManifest();
|
|
51
|
+
if (!Array.isArray(manifest.personas) || manifest.personas.length === 0) {
|
|
52
|
+
throw new Error(`personas/manifest.json must declare a non-empty "personas" array.`);
|
|
53
|
+
}
|
|
54
|
+
const seenIds = new Set();
|
|
55
|
+
const personas = manifest.personas.map((entry) => {
|
|
56
|
+
if (!entry.id || typeof entry.id !== 'string') {
|
|
57
|
+
throw new Error(`personas/manifest.json has an entry with a missing or invalid "id".`);
|
|
58
|
+
}
|
|
59
|
+
if (!entry.description || typeof entry.description !== 'string') {
|
|
60
|
+
throw new Error(`Persona "${entry.id}" is missing a "description" in manifest.json.`);
|
|
61
|
+
}
|
|
62
|
+
if (!Array.isArray(entry.tools) || entry.tools.length === 0) {
|
|
63
|
+
throw new Error(`Persona "${entry.id}" has an empty "tools" array in manifest.json. ` +
|
|
64
|
+
`Every persona must declare at least one narrowed tool.`);
|
|
65
|
+
}
|
|
66
|
+
seenIds.add(entry.id);
|
|
67
|
+
return {
|
|
68
|
+
id: entry.id,
|
|
69
|
+
description: entry.description,
|
|
70
|
+
tools: Object.freeze([...entry.tools]),
|
|
71
|
+
soul: readSoul(entry.id),
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
cachedPersonas = personas;
|
|
75
|
+
return personas;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Renders a persona's prompt for interpolation into a `subagent_spawn(role,
|
|
79
|
+
* objective)` call: the SOUL body, followed by its routing description as
|
|
80
|
+
* additional framing context.
|
|
81
|
+
*
|
|
82
|
+
* @throws {Error} naming the valid ids, if `id` does not match a loaded persona
|
|
83
|
+
*/
|
|
84
|
+
export function renderPersonaPrompt(id) {
|
|
85
|
+
const personas = loadPersonas();
|
|
86
|
+
const persona = personas.find((p) => p.id === id);
|
|
87
|
+
if (!persona) {
|
|
88
|
+
const validIds = personas.map((p) => p.id).join(', ');
|
|
89
|
+
throw new Error(`Unknown persona "${id}". Valid persona ids are: ${validIds}.`);
|
|
90
|
+
}
|
|
91
|
+
return `${persona.soul}\n\n## Role in this task\n\n${persona.description}`;
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=personas.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"personas.js","sourceRoot":"","sources":["../src/personas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,UAAU,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE3D;;;;GAIG;AACH,MAAM,YAAY,GAAG,IAAI,CAAC,UAAU,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC;AAuBxD,SAAS,YAAY;IACnB,MAAM,YAAY,GAAG,IAAI,CAAC,YAAY,EAAE,eAAe,CAAC,CAAC;IACzD,MAAM,GAAG,GAAG,YAAY,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;IAC/C,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAa,CAAC;AACrC,CAAC;AAED,SAAS,QAAQ,CAAC,EAAU;IAC1B,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,EAAE,EAAE,EAAE,SAAS,CAAC,CAAC;IACnD,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;IAC/C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,YAAY,EAAE,sDAAsD,QAAQ,GAAG,EAC/E,EAAE,KAAK,EAAE,GAAG,EAAE,CACf,CAAC;IACJ,CAAC;AACH,CAAC;AAED,IAAI,cAAqC,CAAC;AAE1C;;;;;;GAMG;AACH,MAAM,UAAU,YAAY;IAC1B,IAAI,cAAc;QAAE,OAAO,cAAc,CAAC;IAE1C,MAAM,QAAQ,GAAG,YAAY,EAAE,CAAC;IAChC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxE,MAAM,IAAI,KAAK,CAAC,mEAAmE,CAAC,CAAC;IACvF,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,MAAM,QAAQ,GAAc,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;QAC1D,IAAI,CAAC,KAAK,CAAC,EAAE,IAAI,OAAO,KAAK,CAAC,EAAE,KAAK,QAAQ,EAAE,CAAC;YAC9C,MAAM,IAAI,KAAK,CAAC,qEAAqE,CAAC,CAAC;QACzF,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,WAAW,IAAI,OAAO,KAAK,CAAC,WAAW,KAAK,QAAQ,EAAE,CAAC;YAChE,MAAM,IAAI,KAAK,CAAC,YAAY,KAAK,CAAC,EAAE,gDAAgD,CAAC,CAAC;QACxF,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC5D,MAAM,IAAI,KAAK,CACb,YAAY,KAAK,CAAC,EAAE,iDAAiD;gBACnE,wDAAwD,CAC3D,CAAC;QACJ,CAAC;QACD,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACtB,OAAO;YACL,EAAE,EAAE,KAAK,CAAC,EAAE;YACZ,WAAW,EAAE,KAAK,CAAC,WAAW;YAC9B,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAsB;YAC3D,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;SACzB,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,cAAc,GAAG,QAAQ,CAAC;IAC1B,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,EAAU;IAC5C,MAAM,QAAQ,GAAG,YAAY,EAAE,CAAC;IAChC,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IAClD,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtD,MAAM,IAAI,KAAK,CAAC,oBAAoB,EAAE,6BAA6B,QAAQ,GAAG,CAAC,CAAC;IAClF,CAAC;IAED,OAAO,GAAG,OAAO,CAAC,IAAI,+BAA+B,OAAO,CAAC,WAAW,EAAE,CAAC;AAC7E,CAAC"}
|
package/dist/state.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The .omm/ state store.
|
|
3
|
+
*
|
|
4
|
+
* Every path that reaches the filesystem goes through `resolveWritablePath`, which
|
|
5
|
+
* refuses anything landing in a muse-protected directory or escaping the state
|
|
6
|
+
* root. Keeping that policy in one module means the MCP tool surface cannot
|
|
7
|
+
* accidentally grow a path that skips the check.
|
|
8
|
+
*/
|
|
9
|
+
export interface StateStoreOptions {
|
|
10
|
+
/** Workspace root the store is anchored to. */
|
|
11
|
+
workspaceRoot: string;
|
|
12
|
+
}
|
|
13
|
+
export declare class StateStore {
|
|
14
|
+
readonly workspaceRoot: string;
|
|
15
|
+
constructor({ workspaceRoot }: StateStoreOptions);
|
|
16
|
+
/** Absolute path of this store's state root. */
|
|
17
|
+
get root(): string;
|
|
18
|
+
/**
|
|
19
|
+
* Reads a state file. Returns null when absent, so callers can distinguish
|
|
20
|
+
* "nothing stored yet" from an error without a try/catch.
|
|
21
|
+
*
|
|
22
|
+
* @throws {ProtectedPathError | EscapedStateRootError} when the path is not readable-in-scope
|
|
23
|
+
*/
|
|
24
|
+
read(relativePath: string): string | null;
|
|
25
|
+
/**
|
|
26
|
+
* Writes a state file, creating parent directories inside the state root.
|
|
27
|
+
*
|
|
28
|
+
* @throws {ProtectedPathError | EscapedStateRootError} when the path is out of scope
|
|
29
|
+
*/
|
|
30
|
+
write(relativePath: string, contents: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* Removes a state file or directory. Absent targets are a no-op so callers can
|
|
33
|
+
* clear unconditionally.
|
|
34
|
+
*
|
|
35
|
+
* @throws {ProtectedPathError | EscapedStateRootError} when the path is out of scope
|
|
36
|
+
*/
|
|
37
|
+
clear(relativePath: string): boolean;
|
|
38
|
+
}
|
package/dist/state.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The .omm/ state store.
|
|
3
|
+
*
|
|
4
|
+
* Every path that reaches the filesystem goes through `resolveWritablePath`, which
|
|
5
|
+
* refuses anything landing in a muse-protected directory or escaping the state
|
|
6
|
+
* root. Keeping that policy in one module means the MCP tool surface cannot
|
|
7
|
+
* accidentally grow a path that skips the check.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { dirname } from 'node:path';
|
|
11
|
+
import { ensureStateRoot, resolveWritablePath, stateRoot } from './paths.js';
|
|
12
|
+
export class StateStore {
|
|
13
|
+
workspaceRoot;
|
|
14
|
+
constructor({ workspaceRoot }) {
|
|
15
|
+
this.workspaceRoot = workspaceRoot;
|
|
16
|
+
}
|
|
17
|
+
/** Absolute path of this store's state root. */
|
|
18
|
+
get root() {
|
|
19
|
+
return stateRoot(this.workspaceRoot);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Reads a state file. Returns null when absent, so callers can distinguish
|
|
23
|
+
* "nothing stored yet" from an error without a try/catch.
|
|
24
|
+
*
|
|
25
|
+
* @throws {ProtectedPathError | EscapedStateRootError} when the path is not readable-in-scope
|
|
26
|
+
*/
|
|
27
|
+
read(relativePath) {
|
|
28
|
+
const resolved = resolveWritablePath(this.workspaceRoot, relativePath);
|
|
29
|
+
if (!existsSync(resolved))
|
|
30
|
+
return null;
|
|
31
|
+
return readFileSync(resolved, 'utf8');
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Writes a state file, creating parent directories inside the state root.
|
|
35
|
+
*
|
|
36
|
+
* @throws {ProtectedPathError | EscapedStateRootError} when the path is out of scope
|
|
37
|
+
*/
|
|
38
|
+
write(relativePath, contents) {
|
|
39
|
+
const resolved = resolveWritablePath(this.workspaceRoot, relativePath);
|
|
40
|
+
ensureStateRoot(this.workspaceRoot);
|
|
41
|
+
mkdirSync(dirname(resolved), { recursive: true });
|
|
42
|
+
writeFileSync(resolved, contents, 'utf8');
|
|
43
|
+
return resolved;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Removes a state file or directory. Absent targets are a no-op so callers can
|
|
47
|
+
* clear unconditionally.
|
|
48
|
+
*
|
|
49
|
+
* @throws {ProtectedPathError | EscapedStateRootError} when the path is out of scope
|
|
50
|
+
*/
|
|
51
|
+
clear(relativePath) {
|
|
52
|
+
const resolved = resolveWritablePath(this.workspaceRoot, relativePath);
|
|
53
|
+
if (!existsSync(resolved))
|
|
54
|
+
return false;
|
|
55
|
+
rmSync(resolved, { recursive: true, force: true });
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state.js","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACrF,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAO7E,MAAM,OAAO,UAAU;IACZ,aAAa,CAAS;IAE/B,YAAY,EAAE,aAAa,EAAqB;QAC9C,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;IACrC,CAAC;IAED,gDAAgD;IAChD,IAAI,IAAI;QACN,OAAO,SAAS,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IACvC,CAAC;IAED;;;;;OAKG;IACH,IAAI,CAAC,YAAoB;QACvB,MAAM,QAAQ,GAAG,mBAAmB,CAAC,IAAI,CAAC,aAAa,EAAE,YAAY,CAAC,CAAC;QACvE,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;YAAE,OAAO,IAAI,CAAC;QACvC,OAAO,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACxC,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,YAAoB,EAAE,QAAgB;QAC1C,MAAM,QAAQ,GAAG,mBAAmB,CAAC,IAAI,CAAC,aAAa,EAAE,YAAY,CAAC,CAAC;QACvE,eAAe,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QACpC,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAClD,aAAa,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC1C,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,YAAoB;QACxB,MAAM,QAAQ,GAAG,mBAAmB,CAAC,IAAI,CAAC,aAAa,EAAE,YAAY,CAAC,CAAC;QACvE,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;YAAE,OAAO,KAAK,CAAC;QACxC,MAAM,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACnD,OAAO,IAAI,CAAC;IACd,CAAC;CACF"}
|