supipowers 2.0.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -6
- package/package.json +4 -2
- package/skills/harness/SKILL.md +1 -0
- package/src/bootstrap.ts +5 -133
- package/src/config/defaults.ts +5 -5
- package/src/config/loader.ts +1 -0
- package/src/config/schema.ts +2 -6
- package/src/context-mode/knowledge/store.ts +381 -43
- package/src/context-mode/tools.ts +41 -3
- package/src/deps/registry.ts +1 -12
- package/src/fix-pr/assessment.ts +1 -0
- package/src/fix-pr/prompt-builder.ts +1 -0
- package/src/git/commit.ts +76 -18
- package/src/harness/command.ts +103 -6
- package/src/harness/default-agents/docs.md +39 -0
- package/src/harness/docs/config.ts +29 -0
- package/src/harness/docs/glob-match.ts +27 -0
- package/src/harness/docs/index-renderer.ts +82 -0
- package/src/harness/docs/provenance.ts +125 -0
- package/src/harness/docs/regen-decision.ts +167 -0
- package/src/harness/docs/representative-files.ts +175 -0
- package/src/harness/docs/source-hash.ts +106 -0
- package/src/harness/docs/validator.ts +233 -0
- package/src/harness/hooks/layer-context-inject.ts +35 -1
- package/src/harness/hooks/register.ts +24 -3
- package/src/harness/pipeline.ts +20 -5
- package/src/harness/pr-comment/baseline.ts +105 -0
- package/src/harness/pr-comment/ci-env.ts +120 -0
- package/src/harness/pr-comment/gh-poster.ts +227 -0
- package/src/harness/pr-comment/handler.ts +198 -0
- package/src/harness/pr-comment/render.ts +297 -0
- package/src/harness/pr-comment/status.ts +95 -0
- package/src/harness/pr-comment/types.ts +73 -0
- package/src/harness/pr-comment/workflow-summary.ts +47 -0
- package/src/harness/project-paths.ts +95 -0
- package/src/harness/stages/design.ts +1 -0
- package/src/harness/stages/discover.ts +1 -13
- package/src/harness/stages/docs.ts +708 -0
- package/src/harness/stages/implement-apply.ts +877 -0
- package/src/harness/stages/implement.ts +64 -51
- package/src/harness/stages/plan.ts +25 -16
- package/src/harness/stages/validate.ts +370 -0
- package/src/harness/storage.ts +142 -0
- package/src/harness/tools.ts +130 -0
- package/src/mempalace/bridge.ts +207 -41
- package/src/mempalace/config.ts +10 -4
- package/src/mempalace/format.ts +122 -6
- package/src/mempalace/hooks.ts +204 -56
- package/src/mempalace/installer-helper.ts +18 -4
- package/src/mempalace/python/mempalace_bridge.py +128 -3
- package/src/mempalace/runtime.ts +53 -16
- package/src/mempalace/schema.ts +151 -30
- package/src/mempalace/session-summary.ts +5 -0
- package/src/mempalace/tool.ts +17 -4
- package/src/mempalace/upstream-limits.ts +69 -0
- package/src/planning/approval-flow.ts +25 -2
- package/src/planning/planning-ask-tool.ts +34 -4
- package/src/planning/system-prompt.ts +1 -1
- package/src/tool-catalog/active-tool-controller.ts +0 -22
- package/src/tool-catalog/active-tool-planner.ts +0 -26
- package/src/tool-catalog/tool-groups.ts +1 -9
- package/src/types.ts +87 -8
- package/src/ui-design/session.ts +114 -8
- package/src/utils/executable.ts +10 -1
- package/src/workspace/state-paths.ts +1 -1
- package/src/commands/mcp.ts +0 -814
- package/src/mcp/activation.ts +0 -77
- package/src/mcp/config.ts +0 -223
- package/src/mcp/docs.ts +0 -154
- package/src/mcp/gateway.ts +0 -103
- package/src/mcp/lifecycle.ts +0 -79
- package/src/mcp/manager-tool.ts +0 -104
- package/src/mcp/mcpc.ts +0 -113
- package/src/mcp/registry.ts +0 -98
- package/src/mcp/triggers.ts +0 -62
- package/src/mcp/types.ts +0 -95
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""One-shot JSON bridge for the native supipowers MemPalace tool.
|
|
3
3
|
|
|
4
|
-
Action mapping (auditable API surface)
|
|
4
|
+
Action mapping (auditable API surface):
|
|
5
5
|
- version: stdlib importlib.metadata only; does not import MemPalace runtime modules.
|
|
6
6
|
- 29 MCP-equivalent actions dispatch to ``mempalace.mcp_server.tool_<action>``
|
|
7
7
|
(with the ``traverse → tool_traverse_graph`` rename). The mcp_server
|
|
@@ -129,6 +129,8 @@ def _handle_version(params: Dict[str, Any], options: Dict[str, Any]) -> Dict[str
|
|
|
129
129
|
|
|
130
130
|
def _apply_palace_path(options: Dict[str, Any]) -> None:
|
|
131
131
|
"""Set MEMPALACE_PALACE_PATH so MemPalace's MempalaceConfig picks it up."""
|
|
132
|
+
# NOTE: mutates os.environ — safe only because we run in a fresh process per
|
|
133
|
+
# call. Any future daemon/persistent-process refactor MUST re-evaluate this.
|
|
132
134
|
palace = options.get("palacePath") or options.get("palace")
|
|
133
135
|
if palace and isinstance(palace, str) and palace:
|
|
134
136
|
os.environ["MEMPALACE_PALACE_PATH"] = os.path.expanduser(palace)
|
|
@@ -203,6 +205,42 @@ def _rename(mapping: Dict[str, str]) -> Callable[[Dict[str, Any]], Dict[str, Any
|
|
|
203
205
|
return extract
|
|
204
206
|
|
|
205
207
|
|
|
208
|
+
|
|
209
|
+
# ── diary_write: source_file embedding ───────────────────────────────────
|
|
210
|
+
#
|
|
211
|
+
# tool_diary_write(agent_name, entry, topic, wing) does not accept source_file.
|
|
212
|
+
# When a source_file is provided, embed it as a deterministic first-line prefix
|
|
213
|
+
# "[source: <source_file>]" in the entry text so that diary_read content
|
|
214
|
+
# carries the linkage. This is the Option-2 convention documented in
|
|
215
|
+
# session-summary.ts: the caller supplies source_file in params; the bridge
|
|
216
|
+
# embeds it; the round-trip proves it via content inspection.
|
|
217
|
+
DIARY_SOURCE_PREFIX = "[source: "
|
|
218
|
+
|
|
219
|
+
# Upstream sanitize_content default in mempalace.config (3.3.5). Mirrored in
|
|
220
|
+
# src/mempalace/upstream-limits.ts (MEMPALACE_MAX_CONTENT_LENGTH). Bump both
|
|
221
|
+
# when upgrading.
|
|
222
|
+
_MAX_CONTENT_LENGTH = 100_000
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _diary_write_extractor(params: Dict[str, Any]) -> Dict[str, Any]:
|
|
226
|
+
kwargs = _select("agent_name", "topic", "wing")(params)
|
|
227
|
+
entry: str = params.get("entry") or ""
|
|
228
|
+
source_file = params.get("source_file")
|
|
229
|
+
if source_file and isinstance(source_file, str):
|
|
230
|
+
# MemPalace's tool_diary_write calls sanitize_content(entry), which
|
|
231
|
+
# raises ValueError if len(entry) > MAX_CONTENT_LENGTH. Adding the
|
|
232
|
+
# deterministic `[source: ...]\n` prefix can push a TS-valid entry
|
|
233
|
+
# past that limit and turn a previously-valid request into a domain
|
|
234
|
+
# failure. Reserve the prefix budget here by clipping the entry tail
|
|
235
|
+
# so the prefixed payload always fits.
|
|
236
|
+
prefix = f"{DIARY_SOURCE_PREFIX}{source_file}]\n"
|
|
237
|
+
budget = max(0, _MAX_CONTENT_LENGTH - len(prefix))
|
|
238
|
+
if len(entry) > budget:
|
|
239
|
+
entry = entry[:budget]
|
|
240
|
+
entry = f"{prefix}{entry}"
|
|
241
|
+
kwargs["entry"] = entry
|
|
242
|
+
return kwargs
|
|
243
|
+
|
|
206
244
|
# ── MCP-equivalent action dispatch ───────────────────────────────────────
|
|
207
245
|
#
|
|
208
246
|
# Each entry maps our action -> (function name in mempalace.mcp_server,
|
|
@@ -224,7 +262,7 @@ MCP_TOOL_DISPATCH: Dict[str, "tuple[str, Callable[[Dict[str, Any]], Dict[str, An
|
|
|
224
262
|
"delete_drawer": ("tool_delete_drawer", _select("drawer_id")),
|
|
225
263
|
# Knowledge graph: MemPalace uses `entity` for the subject in queries.
|
|
226
264
|
"kg_query": ("tool_kg_query", _rename({"subject": "entity", "as_of": "as_of", "direction": "direction"})),
|
|
227
|
-
"kg_add": ("tool_kg_add", _select("subject", "predicate", "object", "valid_from", "
|
|
265
|
+
"kg_add": ("tool_kg_add", _select("subject", "predicate", "object", "valid_from", "valid_to", "source_file", "source_drawer_id")),
|
|
228
266
|
"kg_invalidate": ("tool_kg_invalidate", _select("subject", "predicate", "object", "ended")),
|
|
229
267
|
"kg_timeline": ("tool_kg_timeline", _rename({"subject": "entity"})),
|
|
230
268
|
"kg_stats": ("tool_kg_stats", lambda p: {}),
|
|
@@ -239,7 +277,9 @@ MCP_TOOL_DISPATCH: Dict[str, "tuple[str, Callable[[Dict[str, Any]], Dict[str, An
|
|
|
239
277
|
"delete_tunnel": ("tool_delete_tunnel", _select("tunnel_id")),
|
|
240
278
|
# follow_tunnels: MemPalace uses wing/room (not source_wing/source_room).
|
|
241
279
|
"follow_tunnels": ("tool_follow_tunnels", _rename({"source_wing": "wing", "source_room": "room"})),
|
|
242
|
-
|
|
280
|
+
# diary_write: source_file is embedded as a prefix in the entry text by
|
|
281
|
+
# _diary_write_extractor — tool_diary_write does not accept source_file natively.
|
|
282
|
+
"diary_write": ("tool_diary_write", _diary_write_extractor),
|
|
243
283
|
"diary_read": ("tool_diary_read", _select("agent_name", "wing")),
|
|
244
284
|
"hook_settings": ("tool_hook_settings", lambda p: {}),
|
|
245
285
|
"memories_filed_away": ("tool_memories_filed_away", lambda p: {}),
|
|
@@ -287,6 +327,84 @@ def _handle_wake_up(params: Dict[str, Any], options: Dict[str, Any]) -> Dict[str
|
|
|
287
327
|
return _ok({"text": text}, options)
|
|
288
328
|
|
|
289
329
|
|
|
330
|
+
|
|
331
|
+
# ── wake_up_and_search: composite action (saves one python process spawn) ─
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _handle_wake_up_and_search(params: Dict[str, Any], options: Dict[str, Any]) -> Dict[str, Any]:
|
|
335
|
+
"""Batch wake_up + optional search into a single python process.
|
|
336
|
+
|
|
337
|
+
Returns {"wake": {"text": ...}, "search": <tool_search payload or null>}.
|
|
338
|
+
Either half may fail independently; the other half is still returned.
|
|
339
|
+
"""
|
|
340
|
+
_apply_palace_path(options)
|
|
341
|
+
palace = options.get("palacePath")
|
|
342
|
+
|
|
343
|
+
# ── Wake half ─────────────────────────────────────────────────────────────
|
|
344
|
+
wake_result: Any = None
|
|
345
|
+
wake_error: "str | None" = None
|
|
346
|
+
try:
|
|
347
|
+
layers = _import_or_raise("mempalace.layers")
|
|
348
|
+
stack = layers.MemoryStack(palace_path=palace) if palace else layers.MemoryStack()
|
|
349
|
+
wake_kwargs: Dict[str, Any] = {}
|
|
350
|
+
if params.get("wing"):
|
|
351
|
+
wake_kwargs["wing"] = params["wing"]
|
|
352
|
+
text = _wrap_runtime_errors("MemoryStack.wake_up", lambda: stack.wake_up(**wake_kwargs))
|
|
353
|
+
wake_result = {"text": text}
|
|
354
|
+
except BridgeDomainError as exc:
|
|
355
|
+
wake_error = f"{exc.code}: {exc.message}"
|
|
356
|
+
except Exception as exc: # pragma: no cover — defensive isolation
|
|
357
|
+
wake_error = str(exc)
|
|
358
|
+
|
|
359
|
+
# ── Search half (skipped when query is absent or empty) ───────────────────
|
|
360
|
+
search_result: Any = None
|
|
361
|
+
search_error: "str | None" = None
|
|
362
|
+
query = params.get("query")
|
|
363
|
+
if query and isinstance(query, str) and query.strip():
|
|
364
|
+
try:
|
|
365
|
+
module = _import_or_raise("mempalace.mcp_server")
|
|
366
|
+
tool_search = getattr(module, "tool_search", None)
|
|
367
|
+
if not callable(tool_search):
|
|
368
|
+
raise BridgeDomainError(
|
|
369
|
+
"mempalace_missing",
|
|
370
|
+
"mempalace.mcp_server.tool_search is not callable.",
|
|
371
|
+
"Upgrade the managed MemPalace runtime via `/supi:memory setup`.",
|
|
372
|
+
)
|
|
373
|
+
search_kwargs: Dict[str, Any] = {"query": query}
|
|
374
|
+
if params.get("wing"):
|
|
375
|
+
search_kwargs["wing"] = params["wing"]
|
|
376
|
+
if params.get("room"):
|
|
377
|
+
search_kwargs["room"] = params["room"]
|
|
378
|
+
if isinstance(params.get("limit"), int):
|
|
379
|
+
search_kwargs["limit"] = params["limit"]
|
|
380
|
+
raw = _wrap_runtime_errors(
|
|
381
|
+
"mempalace.mcp_server.tool_search",
|
|
382
|
+
lambda: tool_search(**search_kwargs),
|
|
383
|
+
)
|
|
384
|
+
normalized = _to_jsonable(raw)
|
|
385
|
+
if isinstance(normalized, dict) and normalized.get("ok") is False and isinstance(normalized.get("error"), dict):
|
|
386
|
+
err = normalized["error"]
|
|
387
|
+
code = err.get("code") if isinstance(err.get("code"), str) else "mempalace_runtime_error"
|
|
388
|
+
message = err.get("message") if isinstance(err.get("message"), str) else json.dumps(err, sort_keys=True)
|
|
389
|
+
search_error = f"{code}: {message}"
|
|
390
|
+
else:
|
|
391
|
+
search_result = normalized
|
|
392
|
+
except BridgeDomainError as exc:
|
|
393
|
+
# Surface as partial error so the caller can distinguish a real
|
|
394
|
+
# search failure from "no query / no hits". Mirrors the wake half.
|
|
395
|
+
search_error = f"{exc.code}: {exc.message}"
|
|
396
|
+
except Exception as exc: # pragma: no cover — defensive isolation
|
|
397
|
+
# Same contract: never let one half kill the other, but the caller
|
|
398
|
+
# gets a string they can render or log.
|
|
399
|
+
search_error = str(exc)
|
|
400
|
+
|
|
401
|
+
payload: Dict[str, Any] = {"wake": wake_result, "search": search_result}
|
|
402
|
+
if wake_error is not None:
|
|
403
|
+
payload["wake_error"] = wake_error
|
|
404
|
+
if search_error is not None:
|
|
405
|
+
payload["search_error"] = search_error
|
|
406
|
+
return _ok(payload, options)
|
|
407
|
+
|
|
290
408
|
# ── Native CLI args builders ──────────────────────────────────────────────
|
|
291
409
|
|
|
292
410
|
|
|
@@ -307,6 +425,8 @@ def _make_cli_args_mine(params: Dict[str, Any]) -> "list[str]":
|
|
|
307
425
|
args.append("--include-ignored")
|
|
308
426
|
if params.get("no_gitignore"):
|
|
309
427
|
args.append("--no-gitignore")
|
|
428
|
+
if params.get("dry_run"):
|
|
429
|
+
args.append("--dry-run")
|
|
310
430
|
if params.get("extract"):
|
|
311
431
|
args.append("--extract")
|
|
312
432
|
return args
|
|
@@ -331,6 +451,10 @@ def _make_cli_args_repair(params: Dict[str, Any]) -> "list[str]":
|
|
|
331
451
|
args.append("--yes")
|
|
332
452
|
if params.get("mode"):
|
|
333
453
|
args.extend(["--mode", str(params["mode"])])
|
|
454
|
+
if params.get("source"):
|
|
455
|
+
args.extend(["--source", str(params["source"])])
|
|
456
|
+
if params.get("archive_existing"):
|
|
457
|
+
args.append("--archive-existing")
|
|
334
458
|
if params.get("dry_run"):
|
|
335
459
|
args.append("--dry-run")
|
|
336
460
|
return args
|
|
@@ -396,6 +520,7 @@ def _make_cli_handler(action: str) -> Callable[[Dict[str, Any], Dict[str, Any]],
|
|
|
396
520
|
DISPATCH: Dict[str, Callable[[Dict[str, Any], Dict[str, Any]], Dict[str, Any]]] = {
|
|
397
521
|
"version": _handle_version,
|
|
398
522
|
"wake_up": _handle_wake_up,
|
|
523
|
+
"wake_up_and_search": _handle_wake_up_and_search,
|
|
399
524
|
}
|
|
400
525
|
for _action in MCP_TOOL_DISPATCH:
|
|
401
526
|
DISPATCH[_action] = _make_mcp_handler(_action)
|
package/src/mempalace/runtime.ts
CHANGED
|
@@ -19,6 +19,15 @@ export type BridgePathResult =
|
|
|
19
19
|
|
|
20
20
|
export interface BridgePathOptions {
|
|
21
21
|
moduleUrl?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Installed extension package roots. Used when OMP executes temp-copied TS
|
|
24
|
+
* modules that do not preserve adjacent non-TypeScript assets.
|
|
25
|
+
*/
|
|
26
|
+
extensionRoots?: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface BridgePathPlatformPaths {
|
|
30
|
+
agent(...segments: string[]): string;
|
|
22
31
|
}
|
|
23
32
|
|
|
24
33
|
export interface ProcessRunResult {
|
|
@@ -74,6 +83,7 @@ export type RunBridgeRequestResult =
|
|
|
74
83
|
stdoutPreview: string;
|
|
75
84
|
stderrTail: string;
|
|
76
85
|
durationMs: number;
|
|
86
|
+
completion?: Promise<void>;
|
|
77
87
|
};
|
|
78
88
|
|
|
79
89
|
export interface RunBridgeRequestOptions {
|
|
@@ -132,21 +142,39 @@ export interface SetupMempalaceRuntimeOptions {
|
|
|
132
142
|
export function resolveBridgeScriptPath(options: BridgePathOptions = {}): BridgePathResult {
|
|
133
143
|
const moduleUrl = options.moduleUrl ?? import.meta.url;
|
|
134
144
|
const runtimePath = fileURLToPath(moduleUrl);
|
|
135
|
-
const
|
|
145
|
+
const moduleBridgePath = path.join(path.dirname(runtimePath), "python", "mempalace_bridge.py");
|
|
146
|
+
if (fs.existsSync(moduleBridgePath)) {
|
|
147
|
+
return { ok: true, path: moduleBridgePath };
|
|
148
|
+
}
|
|
136
149
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
remediation: "Reinstall supipowers or verify the package includes src/mempalace/python/mempalace_bridge.py.",
|
|
145
|
-
},
|
|
146
|
-
};
|
|
150
|
+
let missingBridgePath = moduleBridgePath;
|
|
151
|
+
for (const extensionRoot of options.extensionRoots ?? []) {
|
|
152
|
+
const extensionBridgePath = path.join(extensionRoot, "src", "mempalace", "python", "mempalace_bridge.py");
|
|
153
|
+
missingBridgePath = extensionBridgePath;
|
|
154
|
+
if (fs.existsSync(extensionBridgePath)) {
|
|
155
|
+
return { ok: true, path: extensionBridgePath };
|
|
156
|
+
}
|
|
147
157
|
}
|
|
148
158
|
|
|
149
|
-
return {
|
|
159
|
+
return {
|
|
160
|
+
ok: false,
|
|
161
|
+
path: missingBridgePath,
|
|
162
|
+
error: {
|
|
163
|
+
code: "bridge_not_found",
|
|
164
|
+
message: `Bundled MemPalace bridge not found at ${missingBridgePath}`,
|
|
165
|
+
remediation: "Reinstall supipowers or verify the package includes src/mempalace/python/mempalace_bridge.py.",
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function resolveInstalledBridgeScriptPath(
|
|
171
|
+
paths: BridgePathPlatformPaths,
|
|
172
|
+
options: BridgePathOptions = {},
|
|
173
|
+
): BridgePathResult {
|
|
174
|
+
return resolveBridgeScriptPath({
|
|
175
|
+
...options,
|
|
176
|
+
extensionRoots: [paths.agent("extensions", "supipowers"), ...(options.extensionRoots ?? [])],
|
|
177
|
+
});
|
|
150
178
|
}
|
|
151
179
|
|
|
152
180
|
export function resolveManagedVenvPaths(
|
|
@@ -296,8 +324,14 @@ export async function runBridgeRequest(options: RunBridgeRequestOptions): Promis
|
|
|
296
324
|
const input = JSON.stringify(options.request);
|
|
297
325
|
const runner = options.runner ?? defaultProcessRunner;
|
|
298
326
|
|
|
327
|
+
const runnerPromise = Promise.resolve().then(() =>
|
|
328
|
+
runner(options.pythonPath, [options.bridgeScriptPath], { input, timeoutMs: options.timeoutMs }),
|
|
329
|
+
);
|
|
330
|
+
const completion = runnerPromise.then(() => {}, () => {});
|
|
331
|
+
|
|
332
|
+
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
299
333
|
const timeout = new Promise<ProcessRunResult>((resolve) => {
|
|
300
|
-
setTimeout(() => resolve({
|
|
334
|
+
timeoutHandle = setTimeout(() => resolve({
|
|
301
335
|
code: -1,
|
|
302
336
|
stdout: "",
|
|
303
337
|
stderr: "",
|
|
@@ -307,9 +341,9 @@ export async function runBridgeRequest(options: RunBridgeRequestOptions): Promis
|
|
|
307
341
|
let runResult: ProcessRunResult;
|
|
308
342
|
try {
|
|
309
343
|
runResult = await Promise.race([
|
|
310
|
-
|
|
344
|
+
runnerPromise,
|
|
311
345
|
timeout,
|
|
312
|
-
]);
|
|
346
|
+
]).finally(() => clearTimeout(timeoutHandle));
|
|
313
347
|
} catch (error) {
|
|
314
348
|
return {
|
|
315
349
|
ok: false,
|
|
@@ -336,6 +370,7 @@ export async function runBridgeRequest(options: RunBridgeRequestOptions): Promis
|
|
|
336
370
|
stdoutPreview: "",
|
|
337
371
|
stderrTail: "",
|
|
338
372
|
durationMs,
|
|
373
|
+
completion,
|
|
339
374
|
};
|
|
340
375
|
}
|
|
341
376
|
|
|
@@ -550,13 +585,15 @@ export function buildMempalaceCliArgs(
|
|
|
550
585
|
const args: string[] = [action];
|
|
551
586
|
if (action === "split") {
|
|
552
587
|
if (params.source_file) args.push(params.source_file);
|
|
553
|
-
} else if (params.dir) {
|
|
588
|
+
} else if (action !== "repair" && params.dir) {
|
|
554
589
|
args.push(params.dir);
|
|
555
590
|
}
|
|
556
591
|
|
|
557
592
|
if (typeof params.limit === "number") args.push("--limit", String(params.limit));
|
|
558
593
|
if (params.mode) args.push("--mode", params.mode);
|
|
594
|
+
if (action === "repair" && params.source) args.push("--source", params.source);
|
|
559
595
|
if (params.extract) args.push("--extract");
|
|
596
|
+
if (action === "repair" && params.archive_existing) args.push("--archive-existing");
|
|
560
597
|
if (params.dry_run) args.push("--dry-run");
|
|
561
598
|
if (params.include_ignored) args.push("--include-ignored");
|
|
562
599
|
if (params.no_gitignore) args.push("--no-gitignore");
|
package/src/mempalace/schema.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MEMPALACE_MAX_CONTENT_LENGTH,
|
|
3
|
+
MEMPALACE_MAX_HOPS,
|
|
4
|
+
MEMPALACE_MAX_NAME_LENGTH,
|
|
5
|
+
MEMPALACE_MAX_QUERY_LENGTH,
|
|
6
|
+
MEMPALACE_MAX_RESULTS,
|
|
7
|
+
} from "./upstream-limits.js";
|
|
8
|
+
|
|
1
9
|
export const MEMPALACE_ACTIONS = [
|
|
2
10
|
"status",
|
|
3
11
|
"list_wings",
|
|
@@ -35,6 +43,7 @@ export const MEMPALACE_ACTIONS = [
|
|
|
35
43
|
"split",
|
|
36
44
|
"repair",
|
|
37
45
|
"wake_up",
|
|
46
|
+
"wake_up_and_search",
|
|
38
47
|
] as const;
|
|
39
48
|
|
|
40
49
|
export type MempalaceAction = typeof MEMPALACE_ACTIONS[number];
|
|
@@ -50,13 +59,14 @@ export interface MempalaceParams {
|
|
|
50
59
|
drawer_id?: string;
|
|
51
60
|
content?: string;
|
|
52
61
|
source_file?: string;
|
|
62
|
+
source_drawer_id?: string;
|
|
53
63
|
added_by?: string;
|
|
54
64
|
subject?: string;
|
|
55
65
|
predicate?: string;
|
|
56
66
|
object?: string;
|
|
57
67
|
valid_from?: string;
|
|
58
68
|
valid_to?: string;
|
|
59
|
-
ended?:
|
|
69
|
+
ended?: string;
|
|
60
70
|
as_of?: string;
|
|
61
71
|
direction?: string;
|
|
62
72
|
start_room?: string;
|
|
@@ -72,8 +82,10 @@ export interface MempalaceParams {
|
|
|
72
82
|
topic?: string;
|
|
73
83
|
dir?: string;
|
|
74
84
|
mode?: string;
|
|
85
|
+
source?: string;
|
|
75
86
|
extract?: boolean;
|
|
76
87
|
dry_run?: boolean;
|
|
88
|
+
archive_existing?: boolean;
|
|
77
89
|
include_ignored?: boolean;
|
|
78
90
|
no_gitignore?: boolean;
|
|
79
91
|
yes?: boolean;
|
|
@@ -94,12 +106,14 @@ const STRING_FIELDS = [
|
|
|
94
106
|
"drawer_id",
|
|
95
107
|
"content",
|
|
96
108
|
"source_file",
|
|
109
|
+
"source_drawer_id",
|
|
97
110
|
"added_by",
|
|
98
111
|
"subject",
|
|
99
112
|
"predicate",
|
|
100
113
|
"object",
|
|
101
114
|
"valid_from",
|
|
102
115
|
"valid_to",
|
|
116
|
+
"ended",
|
|
103
117
|
"as_of",
|
|
104
118
|
"direction",
|
|
105
119
|
"start_room",
|
|
@@ -114,34 +128,65 @@ const STRING_FIELDS = [
|
|
|
114
128
|
"topic",
|
|
115
129
|
"dir",
|
|
116
130
|
"mode",
|
|
131
|
+
"source",
|
|
117
132
|
] as const;
|
|
118
133
|
|
|
119
134
|
const POSITIVE_INTEGER_FIELDS = ["limit", "max_hops", "timeout"] as const;
|
|
135
|
+
const MAX_INTEGER_FIELDS: ReadonlyArray<readonly [keyof MempalaceParams, number]> = [
|
|
136
|
+
["limit", MEMPALACE_MAX_RESULTS],
|
|
137
|
+
["max_hops", MEMPALACE_MAX_HOPS],
|
|
138
|
+
];
|
|
120
139
|
const NON_NEGATIVE_INTEGER_FIELDS = ["offset"] as const;
|
|
121
|
-
const BOOLEAN_FIELDS = ["
|
|
140
|
+
const BOOLEAN_FIELDS = ["extract", "dry_run", "archive_existing", "include_ignored", "no_gitignore", "yes"] as const;
|
|
141
|
+
|
|
142
|
+
// Per-field maximum string lengths. Each entry mirrors the JSON schema
|
|
143
|
+
// `maxLength` and corresponds to an upstream MemPalace sanitizer bound:
|
|
144
|
+
// - sanitize_query (tool_search) silently truncates anything beyond
|
|
145
|
+
// MAX_QUERY_LENGTH, so callers can't tell their long query was clipped.
|
|
146
|
+
// - sanitize_name / sanitize_kg_value / sanitize_content all raise
|
|
147
|
+
// ValueError beyond their limits, which surfaces as a domain error
|
|
148
|
+
// from the python bridge instead of at the TS boundary.
|
|
149
|
+
// Enforcing here gives every overlong field the same `field exceeds <N>
|
|
150
|
+
// characters` error path before the python child is spawned.
|
|
151
|
+
const MAX_LENGTH_FIELDS: ReadonlyArray<readonly [keyof MempalaceParams, number]> = [
|
|
152
|
+
["query", MEMPALACE_MAX_QUERY_LENGTH],
|
|
153
|
+
["content", MEMPALACE_MAX_CONTENT_LENGTH],
|
|
154
|
+
["entry", MEMPALACE_MAX_CONTENT_LENGTH],
|
|
155
|
+
["wing", MEMPALACE_MAX_NAME_LENGTH],
|
|
156
|
+
["room", MEMPALACE_MAX_NAME_LENGTH],
|
|
157
|
+
["subject", MEMPALACE_MAX_NAME_LENGTH],
|
|
158
|
+
["predicate", MEMPALACE_MAX_NAME_LENGTH],
|
|
159
|
+
["object", MEMPALACE_MAX_NAME_LENGTH],
|
|
160
|
+
["start_room", MEMPALACE_MAX_NAME_LENGTH],
|
|
161
|
+
["source_wing", MEMPALACE_MAX_NAME_LENGTH],
|
|
162
|
+
["source_room", MEMPALACE_MAX_NAME_LENGTH],
|
|
163
|
+
["target_wing", MEMPALACE_MAX_NAME_LENGTH],
|
|
164
|
+
["target_room", MEMPALACE_MAX_NAME_LENGTH],
|
|
165
|
+
["agent_name", MEMPALACE_MAX_NAME_LENGTH],
|
|
166
|
+
["topic", MEMPALACE_MAX_NAME_LENGTH],
|
|
167
|
+
] as const;
|
|
122
168
|
|
|
123
|
-
const REQUIRED_FIELDS: Partial<Record<MempalaceAction, readonly (keyof MempalaceParams)[]>> = {
|
|
169
|
+
export const REQUIRED_FIELDS: Partial<Record<MempalaceAction, readonly (keyof MempalaceParams)[]>> = {
|
|
124
170
|
search: ["query"],
|
|
125
171
|
check_duplicate: ["content"],
|
|
126
172
|
get_drawer: ["drawer_id"],
|
|
127
173
|
list_drawers: ["wing"],
|
|
128
|
-
add_drawer: ["content"],
|
|
174
|
+
add_drawer: ["wing", "room", "content"],
|
|
129
175
|
update_drawer: ["drawer_id", "content"],
|
|
130
176
|
delete_drawer: ["drawer_id"],
|
|
131
177
|
kg_query: ["subject"],
|
|
132
178
|
kg_add: ["subject", "predicate", "object"],
|
|
133
|
-
kg_invalidate: ["subject", "predicate"],
|
|
179
|
+
kg_invalidate: ["subject", "predicate", "object"],
|
|
134
180
|
kg_timeline: ["subject"],
|
|
135
181
|
traverse: ["start_room"],
|
|
136
|
-
find_tunnels: ["source_wing", "source_room"],
|
|
137
182
|
create_tunnel: ["source_wing", "source_room", "target_wing", "target_room", "label"],
|
|
138
183
|
delete_tunnel: ["tunnel_id"],
|
|
139
184
|
follow_tunnels: ["source_wing", "source_room"],
|
|
140
|
-
diary_write: ["entry"],
|
|
185
|
+
diary_write: ["entry", "agent_name"],
|
|
186
|
+
diary_read: ["agent_name"],
|
|
141
187
|
init: ["dir"],
|
|
142
188
|
mine: ["dir"],
|
|
143
189
|
split: ["source_file"],
|
|
144
|
-
repair: ["dir"],
|
|
145
190
|
};
|
|
146
191
|
|
|
147
192
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -156,6 +201,44 @@ function isNonEmptyString(value: unknown): value is string {
|
|
|
156
201
|
return typeof value === "string" && value.trim().length > 0;
|
|
157
202
|
}
|
|
158
203
|
|
|
204
|
+
const ISO_DATE_PART_PATTERN = "\\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\\d|3[01])";
|
|
205
|
+
const ISO_UTC_DATETIME_PATTERN = `${ISO_DATE_PART_PATTERN}T(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:Z|\\+00:00)`;
|
|
206
|
+
const ISO_TEMPORAL_PATTERN = `^(?:${ISO_DATE_PART_PATTERN}|${ISO_UTC_DATETIME_PATTERN})$`;
|
|
207
|
+
const ISO_TEMPORAL_DESCRIPTION =
|
|
208
|
+
"ISO temporal string accepted by MemPalace: YYYY-MM-DD, YYYY-MM-DDTHH:MM:SSZ, or YYYY-MM-DDTHH:MM:SS+00:00.";
|
|
209
|
+
const ISO_TEMPORAL_SCHEMA = {
|
|
210
|
+
type: "string",
|
|
211
|
+
pattern: ISO_TEMPORAL_PATTERN,
|
|
212
|
+
description: ISO_TEMPORAL_DESCRIPTION,
|
|
213
|
+
} as const;
|
|
214
|
+
const ISO_TEMPORAL_REGEX = new RegExp(ISO_TEMPORAL_PATTERN);
|
|
215
|
+
const ISO_TEMPORAL_FIELDS = ["as_of", "valid_from", "valid_to", "ended"] as const;
|
|
216
|
+
const DAYS_IN_MONTH = [31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] as const;
|
|
217
|
+
|
|
218
|
+
function fixedInt(value: string, start: number, end: number): number {
|
|
219
|
+
let out = 0;
|
|
220
|
+
for (let i = start; i < end; i += 1) {
|
|
221
|
+
out = out * 10 + value.charCodeAt(i) - 48;
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function isLeapYear(year: number): boolean {
|
|
227
|
+
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function isValidIsoTemporal(value: string): boolean {
|
|
231
|
+
if (!ISO_TEMPORAL_REGEX.test(value)) return false;
|
|
232
|
+
|
|
233
|
+
const year = fixedInt(value, 0, 4);
|
|
234
|
+
const month = fixedInt(value, 5, 7);
|
|
235
|
+
const day = fixedInt(value, 8, 10);
|
|
236
|
+
if (year < 1) return false;
|
|
237
|
+
|
|
238
|
+
const daysInMonth = month === 2 ? (isLeapYear(year) ? 29 : 28) : DAYS_IN_MONTH[month - 1];
|
|
239
|
+
return day <= daysInMonth;
|
|
240
|
+
}
|
|
241
|
+
|
|
159
242
|
export const mempalaceToolParameters = {
|
|
160
243
|
type: "object",
|
|
161
244
|
additionalProperties: false,
|
|
@@ -166,38 +249,45 @@ export const mempalaceToolParameters = {
|
|
|
166
249
|
description: "Action to dispatch.",
|
|
167
250
|
},
|
|
168
251
|
palace: { type: "string" },
|
|
169
|
-
wing: {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
252
|
+
wing: {
|
|
253
|
+
type: "string",
|
|
254
|
+
maxLength: MEMPALACE_MAX_NAME_LENGTH,
|
|
255
|
+
description: "Wing name. Required for list_drawers to scope the query and avoid an accidental full-palace dump.",
|
|
256
|
+
},
|
|
257
|
+
room: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
|
|
258
|
+
query: { type: "string", maxLength: MEMPALACE_MAX_QUERY_LENGTH },
|
|
259
|
+
limit: { type: "integer", minimum: 1, maximum: MEMPALACE_MAX_RESULTS },
|
|
260
|
+
offset: { type: "integer", minimum: 0, description: "Zero-based pagination offset (use with limit for list_drawers and similar listing actions)." },
|
|
174
261
|
drawer_id: { type: "string" },
|
|
175
|
-
content: { type: "string" },
|
|
262
|
+
content: { type: "string", maxLength: MEMPALACE_MAX_CONTENT_LENGTH },
|
|
176
263
|
source_file: { type: "string" },
|
|
264
|
+
source_drawer_id: { type: "string" },
|
|
177
265
|
added_by: { type: "string" },
|
|
178
|
-
subject: { type: "string" },
|
|
179
|
-
predicate: { type: "string" },
|
|
180
|
-
object: { type: "string" },
|
|
181
|
-
valid_from:
|
|
182
|
-
valid_to:
|
|
183
|
-
ended:
|
|
184
|
-
as_of:
|
|
266
|
+
subject: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
|
|
267
|
+
predicate: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
|
|
268
|
+
object: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
|
|
269
|
+
valid_from: ISO_TEMPORAL_SCHEMA,
|
|
270
|
+
valid_to: ISO_TEMPORAL_SCHEMA,
|
|
271
|
+
ended: ISO_TEMPORAL_SCHEMA,
|
|
272
|
+
as_of: ISO_TEMPORAL_SCHEMA,
|
|
185
273
|
direction: { type: "string" },
|
|
186
|
-
start_room: { type: "string" },
|
|
187
|
-
max_hops: { type: "integer", minimum: 1 },
|
|
188
|
-
source_wing: { type: "string" },
|
|
189
|
-
source_room: { type: "string" },
|
|
190
|
-
target_wing: { type: "string" },
|
|
191
|
-
target_room: { type: "string" },
|
|
274
|
+
start_room: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
|
|
275
|
+
max_hops: { type: "integer", minimum: 1, maximum: MEMPALACE_MAX_HOPS },
|
|
276
|
+
source_wing: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
|
|
277
|
+
source_room: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
|
|
278
|
+
target_wing: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
|
|
279
|
+
target_room: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
|
|
192
280
|
label: { type: "string" },
|
|
193
281
|
tunnel_id: { type: "string" },
|
|
194
|
-
agent_name: { type: "string" },
|
|
195
|
-
entry: { type: "string" },
|
|
196
|
-
topic: { type: "string" },
|
|
282
|
+
agent_name: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
|
|
283
|
+
entry: { type: "string", maxLength: MEMPALACE_MAX_CONTENT_LENGTH },
|
|
284
|
+
topic: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
|
|
197
285
|
dir: { type: "string" },
|
|
198
286
|
mode: { type: "string" },
|
|
287
|
+
source: { type: "string" },
|
|
199
288
|
extract: { type: "boolean" },
|
|
200
289
|
dry_run: { type: "boolean" },
|
|
290
|
+
archive_existing: { type: "boolean" },
|
|
201
291
|
include_ignored: { type: "boolean" },
|
|
202
292
|
no_gitignore: { type: "boolean" },
|
|
203
293
|
yes: { type: "boolean" },
|
|
@@ -254,6 +344,37 @@ export function validateMempalaceParams(params: unknown): MempalaceValidationRes
|
|
|
254
344
|
}
|
|
255
345
|
}
|
|
256
346
|
|
|
347
|
+
for (const [field, max] of MAX_INTEGER_FIELDS) {
|
|
348
|
+
const value = params[field];
|
|
349
|
+
if (typeof value === "number" && Number.isInteger(value) && value > max) {
|
|
350
|
+
errors.push(`${field} exceeds maximum of ${max}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
for (const [field, max] of MAX_LENGTH_FIELDS) {
|
|
355
|
+
const value = params[field];
|
|
356
|
+
if (typeof value === "string" && value.length > max) {
|
|
357
|
+
errors.push(`${field} exceeds maximum length of ${max} characters`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ISO temporal fields: callers commonly pass `" 2026-05-13 "` from logs or
|
|
362
|
+
// env vars — strip surrounding whitespace before validating so we accept
|
|
363
|
+
// the same shape MemPalace's downstream sqlite comparison would. Empty
|
|
364
|
+
// strings are still rejected (semantically not a date, and would be stored
|
|
365
|
+
// as garbage in the kg.triples valid_from/valid_to columns).
|
|
366
|
+
for (const field of ISO_TEMPORAL_FIELDS) {
|
|
367
|
+
const value = params[field];
|
|
368
|
+
if (typeof value !== "string") continue;
|
|
369
|
+
const trimmed = value.trim();
|
|
370
|
+
if (trimmed !== value) {
|
|
371
|
+
(params as Record<string, unknown>)[field] = trimmed;
|
|
372
|
+
}
|
|
373
|
+
if (!isValidIsoTemporal(trimmed)) {
|
|
374
|
+
errors.push(`${field} must be an ISO temporal string accepted by MemPalace`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
257
378
|
for (const field of REQUIRED_FIELDS[action] ?? []) {
|
|
258
379
|
if (!isNonEmptyString(params[field])) {
|
|
259
380
|
errors.push(`${field} is required for action ${action}`);
|
|
@@ -185,6 +185,11 @@ export function buildCompactionCheckpoint(options: SessionSummaryOptions): Compa
|
|
|
185
185
|
|
|
186
186
|
export function buildShutdownDiary(options: SessionSummaryOptions): ShutdownDiary {
|
|
187
187
|
const { body, now } = buildBody("MemPalace shutdown diary", "shutdown", options);
|
|
188
|
+
// source_file convention: the bridge's Python layer (mempalace_bridge.py) embeds
|
|
189
|
+
// the source_file value as a deterministic first-line prefix in the entry text:
|
|
190
|
+
// "[source: <source_file>]\n<entry>"
|
|
191
|
+
// tool_diary_write does not accept source_file natively; the prefix is how the
|
|
192
|
+
// round-trip proves the linkage (diary_read returns the full document text).
|
|
188
193
|
return {
|
|
189
194
|
entry: body,
|
|
190
195
|
metadata: {
|
package/src/mempalace/tool.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { resolveMempalaceConfig, type ResolvedMempalaceConfig } from "./config.j
|
|
|
5
5
|
import { formatMempalaceError, formatMempalaceResult } from "./format.js";
|
|
6
6
|
import { mempalaceToolParameters, validateMempalaceParams, type MempalaceParams } from "./schema.js";
|
|
7
7
|
import {
|
|
8
|
-
|
|
8
|
+
resolveInstalledBridgeScriptPath,
|
|
9
9
|
setupMempalaceRuntime,
|
|
10
10
|
type BridgePathResult,
|
|
11
11
|
type SetupMempalaceRuntimeOptions,
|
|
@@ -55,10 +55,11 @@ async function executeSetup(
|
|
|
55
55
|
resolved: ResolvedMempalaceConfig,
|
|
56
56
|
cwd: string,
|
|
57
57
|
managedBinDir: string,
|
|
58
|
+
defaultResolveBridgeScriptPath: () => BridgePathResult,
|
|
58
59
|
deps: MempalaceToolDeps,
|
|
59
60
|
onUpdate: unknown,
|
|
60
61
|
): Promise<{ content: Array<{ type: "text"; text: string }>; details: Record<string, unknown> }> {
|
|
61
|
-
const bridgePath = (deps.resolveBridgeScriptPath ??
|
|
62
|
+
const bridgePath = (deps.resolveBridgeScriptPath ?? defaultResolveBridgeScriptPath)();
|
|
62
63
|
if (!bridgePath.ok) {
|
|
63
64
|
const formatted = formatMempalaceError(bridgePath.error, {
|
|
64
65
|
ok: false,
|
|
@@ -121,6 +122,10 @@ export function registerMempalaceTool(
|
|
|
121
122
|
return;
|
|
122
123
|
}
|
|
123
124
|
if (!snapshot.ready) return;
|
|
125
|
+
const bridgeRuntime = {
|
|
126
|
+
resolveBridgeScriptPath: () => resolveInstalledBridgeScriptPath(platform.paths),
|
|
127
|
+
};
|
|
128
|
+
|
|
124
129
|
|
|
125
130
|
platform.registerTool({
|
|
126
131
|
name: "mempalace",
|
|
@@ -143,12 +148,20 @@ export function registerMempalaceTool(
|
|
|
143
148
|
const params = validation.params;
|
|
144
149
|
|
|
145
150
|
if (params.action === "setup") {
|
|
146
|
-
return await executeSetup(
|
|
151
|
+
return await executeSetup(
|
|
152
|
+
params,
|
|
153
|
+
resolved,
|
|
154
|
+
cwd,
|
|
155
|
+
platform.paths.global("bin"),
|
|
156
|
+
bridgeRuntime.resolveBridgeScriptPath,
|
|
157
|
+
deps,
|
|
158
|
+
onUpdate,
|
|
159
|
+
);
|
|
147
160
|
}
|
|
148
161
|
|
|
149
162
|
const bridge = deps.createBridge
|
|
150
163
|
? deps.createBridge(resolved, cwd)
|
|
151
|
-
: createMempalaceBridge({ cwd, config: resolved });
|
|
164
|
+
: createMempalaceBridge({ cwd, config: resolved, runtime: bridgeRuntime });
|
|
152
165
|
const result = await bridge.execute(params);
|
|
153
166
|
|
|
154
167
|
if (!result.ok) {
|