@agentproto/runtime 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/LICENSE +21 -0
- package/README.md +88 -0
- package/dist/config.d.ts +144 -0
- package/dist/config.mjs +76 -0
- package/dist/config.mjs.map +1 -0
- package/dist/conversations.d.ts +60 -0
- package/dist/conversations.mjs +146 -0
- package/dist/conversations.mjs.map +1 -0
- package/dist/heartbeat-COGpMrJS.d.ts +120 -0
- package/dist/heartbeat.d.ts +2 -0
- package/dist/heartbeat.mjs +185 -0
- package/dist/heartbeat.mjs.map +1 -0
- package/dist/index.d.ts +546 -0
- package/dist/index.mjs +4350 -0
- package/dist/index.mjs.map +1 -0
- package/dist/mcp-imports.d.ts +117 -0
- package/dist/mcp-imports.mjs +82 -0
- package/dist/mcp-imports.mjs.map +1 -0
- package/dist/resume-strategies.d.ts +66 -0
- package/dist/resume-strategies.mjs +64 -0
- package/dist/resume-strategies.mjs.map +1 -0
- package/dist/workspace-fs.d.ts +26 -0
- package/dist/workspace-fs.mjs +60 -0
- package/dist/workspace-fs.mjs.map +1 -0
- package/dist/workspaces-config.d.ts +81 -0
- package/dist/workspaces-config.mjs +136 -0
- package/dist/workspaces-config.mjs.map +1 -0
- package/package.json +103 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discover MCP servers already configured in the user's local agent
|
|
3
|
+
* tooling — claude-code, cursor, goose, etc. — so the daemon can
|
|
4
|
+
* surface them in the UI without making the user re-enter the same
|
|
5
|
+
* credentials and URLs they already painstakingly configured for
|
|
6
|
+
* one host.
|
|
7
|
+
*
|
|
8
|
+
* v1 is read-only discovery: we report what's there with source
|
|
9
|
+
* attribution. Future v1.1 ships an "import to daemon" verb that
|
|
10
|
+
* proxies the discovered server through the daemon's own /mcp
|
|
11
|
+
* endpoint so every operator (cloud Guilde, local agents) sees a
|
|
12
|
+
* unified set without per-host config drift.
|
|
13
|
+
*
|
|
14
|
+
* Sources scanned (in priority order):
|
|
15
|
+
* - ~/.claude.json (claude-code) — `mcpServers` top-level + nested
|
|
16
|
+
* under `projects[<path>].mcpServers`. Most users land here.
|
|
17
|
+
* - ~/.cursor/mcp.json (cursor) — flat `mcpServers` map.
|
|
18
|
+
* - ~/.config/goose/config.yaml (goose) — TOML/YAML hybrid; we
|
|
19
|
+
* only handle the YAML form for now.
|
|
20
|
+
* - any `.mcp.json` under the user's home that follows the
|
|
21
|
+
* standard mcpServers shape.
|
|
22
|
+
*
|
|
23
|
+
* Each source contributes 0..N entries. Errors are caught + logged
|
|
24
|
+
* (broken JSON, missing files, permission denied) — partial
|
|
25
|
+
* discovery beats failing the whole listing on one bad file.
|
|
26
|
+
*/
|
|
27
|
+
interface DiscoveredMcp {
|
|
28
|
+
/** Stable id within (source, scope, name) — for de-dupe and
|
|
29
|
+
* cross-call identity (UI treat as React key). */
|
|
30
|
+
id: string;
|
|
31
|
+
/** Where we found it. Lets the UI render a source badge. */
|
|
32
|
+
source: "claude-code" | "cursor" | "goose" | "workspace";
|
|
33
|
+
/** "global" when the entry sits at the top level of the host's
|
|
34
|
+
* config; "project:<path>" when it's scoped to a single
|
|
35
|
+
* workspace. claude-code in particular nests per-project. */
|
|
36
|
+
scope: string;
|
|
37
|
+
/** The host's name for the entry (key in the mcpServers map). */
|
|
38
|
+
name: string;
|
|
39
|
+
/** Wire transport — `stdio` (command + args) or `http` / `sse` (url). */
|
|
40
|
+
type: "stdio" | "http" | "sse" | "unknown";
|
|
41
|
+
/** stdio entries: argv to spawn. */
|
|
42
|
+
command?: string;
|
|
43
|
+
args?: string[];
|
|
44
|
+
/** stdio entries: env injected into the child. */
|
|
45
|
+
env?: Record<string, string>;
|
|
46
|
+
/** http/sse entries: target URL. */
|
|
47
|
+
url?: string;
|
|
48
|
+
/** http/sse entries: extra headers (Authorization, etc.). */
|
|
49
|
+
headers?: Record<string, string>;
|
|
50
|
+
/** Free-form tags from the source config — claude-code's project
|
|
51
|
+
* scope is the most common. */
|
|
52
|
+
tags?: string[];
|
|
53
|
+
/** Reason field when `type === "unknown"` — surfaces why the
|
|
54
|
+
* parser couldn't classify the entry (e.g. neither command nor
|
|
55
|
+
* url present). */
|
|
56
|
+
parseNote?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* `~/.agentproto/imported-mcps.json` — the user's curated set of
|
|
61
|
+
* discovered MCPs they want the daemon to know about. v1 is just
|
|
62
|
+
* persistence + read-side; the actual MCP-proxy implementation
|
|
63
|
+
* (where the daemon's /mcp endpoint aggregates the imported
|
|
64
|
+
* servers' tools under namespace prefixes) is v2 work.
|
|
65
|
+
*
|
|
66
|
+
* Purpose: today, "I see you have chrome-devtools in claude" is
|
|
67
|
+
* read-only. Once a user imports it, the operator agent can refer
|
|
68
|
+
* to it ("the user said it's enabled — call it") and a future
|
|
69
|
+
* proxy layer can actually expose it. We persist the choice now so
|
|
70
|
+
* the data is in place when the proxy lands.
|
|
71
|
+
*
|
|
72
|
+
* File shape:
|
|
73
|
+
* {
|
|
74
|
+
* "version": 1,
|
|
75
|
+
* "imports": [
|
|
76
|
+
* {
|
|
77
|
+
* "id": "claude-code:project:/path:chrome-devtools",
|
|
78
|
+
* "alias": "chrome-devtools",
|
|
79
|
+
* "addedAt": "2026-05-10T...",
|
|
80
|
+
* "snapshot": { ...the DiscoveredMcp at import time }
|
|
81
|
+
* }
|
|
82
|
+
* ]
|
|
83
|
+
* }
|
|
84
|
+
*
|
|
85
|
+
* Snapshotting at import time keeps the daemon resilient to the
|
|
86
|
+
* source config getting deleted (user runs `claude mcp remove ...`).
|
|
87
|
+
* The imported entry stays usable; only the next discovery pass
|
|
88
|
+
* stops listing it as "available to import."
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
declare const IMPORTED_MCPS_PATH: () => string;
|
|
92
|
+
declare const IMPORTED_MCPS_VERSION: 1;
|
|
93
|
+
interface ImportedMcpEntry {
|
|
94
|
+
id: string;
|
|
95
|
+
alias: string;
|
|
96
|
+
addedAt: string;
|
|
97
|
+
snapshot: DiscoveredMcp;
|
|
98
|
+
}
|
|
99
|
+
interface ImportedMcpsConfig {
|
|
100
|
+
version: typeof IMPORTED_MCPS_VERSION;
|
|
101
|
+
imports: ImportedMcpEntry[];
|
|
102
|
+
}
|
|
103
|
+
declare function loadImportedMcps(path?: string): Promise<ImportedMcpsConfig>;
|
|
104
|
+
declare function saveImportedMcps(config: ImportedMcpsConfig, path?: string): Promise<void>;
|
|
105
|
+
/**
|
|
106
|
+
* Add (or replace) an import. Snapshots the discovered MCP at
|
|
107
|
+
* import time so the entry stays usable when the source config
|
|
108
|
+
* disappears.
|
|
109
|
+
*/
|
|
110
|
+
declare function addImport(config: ImportedMcpsConfig, input: {
|
|
111
|
+
snapshot: DiscoveredMcp;
|
|
112
|
+
alias?: string;
|
|
113
|
+
}): ImportedMcpsConfig;
|
|
114
|
+
declare function removeImport(config: ImportedMcpsConfig, id: string): ImportedMcpsConfig;
|
|
115
|
+
declare function findImport(config: ImportedMcpsConfig, id: string): ImportedMcpEntry | undefined;
|
|
116
|
+
|
|
117
|
+
export { IMPORTED_MCPS_PATH, IMPORTED_MCPS_VERSION, type ImportedMcpEntry, type ImportedMcpsConfig, addImport, findImport, loadImportedMcps, removeImport, saveImportedMcps };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { promises } from 'fs';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { resolve, dirname } from 'path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @agentproto/runtime v0.1.0-alpha
|
|
7
|
+
* Long-running gateway: MCP server + HTTP transport + HEARTBEAT autonomy + conversation persistence over a workspace dir.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
var IMPORTED_MCPS_PATH = () => resolve(homedir(), ".agentproto", "imported-mcps.json");
|
|
11
|
+
var IMPORTED_MCPS_VERSION = 1;
|
|
12
|
+
var EMPTY = {
|
|
13
|
+
version: IMPORTED_MCPS_VERSION,
|
|
14
|
+
imports: []
|
|
15
|
+
};
|
|
16
|
+
async function loadImportedMcps(path = IMPORTED_MCPS_PATH()) {
|
|
17
|
+
let raw;
|
|
18
|
+
try {
|
|
19
|
+
raw = await promises.readFile(path, "utf8");
|
|
20
|
+
} catch (err) {
|
|
21
|
+
if (err.code === "ENOENT") return EMPTY;
|
|
22
|
+
throw err;
|
|
23
|
+
}
|
|
24
|
+
let parsed;
|
|
25
|
+
try {
|
|
26
|
+
parsed = JSON.parse(raw);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`agentproto: ${path} is not valid JSON (${err instanceof Error ? err.message : String(err)}). Delete or fix manually.`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return normalize(parsed);
|
|
33
|
+
}
|
|
34
|
+
async function saveImportedMcps(config, path = IMPORTED_MCPS_PATH()) {
|
|
35
|
+
await promises.mkdir(dirname(path), { recursive: true });
|
|
36
|
+
const tmp = `${path}.tmp.${process.pid}`;
|
|
37
|
+
await promises.writeFile(tmp, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
38
|
+
await promises.rename(tmp, path);
|
|
39
|
+
}
|
|
40
|
+
function addImport(config, input) {
|
|
41
|
+
const alias = (input.alias ?? input.snapshot.name).trim();
|
|
42
|
+
if (!alias) {
|
|
43
|
+
throw new Error("addImport: alias resolves to empty string");
|
|
44
|
+
}
|
|
45
|
+
const addedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
46
|
+
const next = {
|
|
47
|
+
id: input.snapshot.id,
|
|
48
|
+
alias,
|
|
49
|
+
addedAt,
|
|
50
|
+
snapshot: input.snapshot
|
|
51
|
+
};
|
|
52
|
+
const others = config.imports.filter((e) => e.id !== input.snapshot.id);
|
|
53
|
+
return { ...config, imports: [...others, next] };
|
|
54
|
+
}
|
|
55
|
+
function removeImport(config, id) {
|
|
56
|
+
return { ...config, imports: config.imports.filter((e) => e.id !== id) };
|
|
57
|
+
}
|
|
58
|
+
function findImport(config, id) {
|
|
59
|
+
return config.imports.find((e) => e.id === id);
|
|
60
|
+
}
|
|
61
|
+
function normalize(parsed) {
|
|
62
|
+
if (!parsed || typeof parsed !== "object") return EMPTY;
|
|
63
|
+
const obj = parsed;
|
|
64
|
+
const imports = [];
|
|
65
|
+
if (Array.isArray(obj.imports)) {
|
|
66
|
+
for (const entry of obj.imports) {
|
|
67
|
+
if (!entry || typeof entry !== "object") continue;
|
|
68
|
+
const e = entry;
|
|
69
|
+
const id = typeof e.id === "string" ? e.id : "";
|
|
70
|
+
const alias = typeof e.alias === "string" ? e.alias : id;
|
|
71
|
+
const addedAt = typeof e.addedAt === "string" ? e.addedAt : (/* @__PURE__ */ new Date()).toISOString();
|
|
72
|
+
const snapshot = e.snapshot;
|
|
73
|
+
if (!id || !snapshot) continue;
|
|
74
|
+
imports.push({ id, alias, addedAt, snapshot });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { version: IMPORTED_MCPS_VERSION, imports };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export { IMPORTED_MCPS_PATH, IMPORTED_MCPS_VERSION, addImport, findImport, loadImportedMcps, removeImport, saveImportedMcps };
|
|
81
|
+
//# sourceMappingURL=mcp-imports.mjs.map
|
|
82
|
+
//# sourceMappingURL=mcp-imports.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/mcp-imports.ts"],"names":["resolvePath","fs"],"mappings":";;;;;;;;;AAqCO,IAAM,qBAAqB,MAChCA,OAAA,CAAY,OAAA,EAAQ,EAAG,eAAe,oBAAoB;AAErD,IAAM,qBAAA,GAAwB;AAcrC,IAAM,KAAA,GAA4B;AAAA,EAChC,OAAA,EAAS,qBAAA;AAAA,EACT,SAAS;AACX,CAAA;AAEA,eAAsB,gBAAA,CACpB,IAAA,GAAe,kBAAA,EAAmB,EACL;AAC7B,EAAA,IAAI,GAAA;AACJ,EAAA,IAAI;AACF,IAAA,GAAA,GAAM,MAAMC,QAAA,CAAG,QAAA,CAAS,IAAA,EAAM,MAAM,CAAA;AAAA,EACtC,SAAS,GAAA,EAAK;AACZ,IAAA,IAAK,GAAA,CAA8B,IAAA,KAAS,QAAA,EAAU,OAAO,KAAA;AAC7D,IAAA,MAAM,GAAA;AAAA,EACR;AACA,EAAA,IAAI,MAAA;AACJ,EAAA,IAAI;AACF,IAAA,MAAA,GAAS,IAAA,CAAK,MAAM,GAAG,CAAA;AAAA,EACzB,SAAS,GAAA,EAAK;AACZ,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,YAAA,EAAe,IAAI,CAAA,oBAAA,EACjB,GAAA,YAAe,QAAQ,GAAA,CAAI,OAAA,GAAU,MAAA,CAAO,GAAG,CACjD,CAAA,0BAAA;AAAA,KACF;AAAA,EACF;AACA,EAAA,OAAO,UAAU,MAAM,CAAA;AACzB;AAEA,eAAsB,gBAAA,CACpB,MAAA,EACA,IAAA,GAAe,kBAAA,EAAmB,EACnB;AACf,EAAA,MAAMA,QAAA,CAAG,MAAM,OAAA,CAAQ,IAAI,GAAG,EAAE,SAAA,EAAW,MAAM,CAAA;AACjD,EAAA,MAAM,GAAA,GAAM,CAAA,EAAG,IAAI,CAAA,KAAA,EAAQ,QAAQ,GAAG,CAAA,CAAA;AACtC,EAAA,MAAMA,QAAA,CAAG,SAAA,CAAU,GAAA,EAAK,IAAA,CAAK,SAAA,CAAU,QAAQ,IAAA,EAAM,CAAC,CAAA,GAAI,IAAA,EAAM,MAAM,CAAA;AACtE,EAAA,MAAMA,QAAA,CAAG,MAAA,CAAO,GAAA,EAAK,IAAI,CAAA;AAC3B;AAOO,SAAS,SAAA,CACd,QACA,KAAA,EACoB;AACpB,EAAA,MAAM,SAAS,KAAA,CAAM,KAAA,IAAS,KAAA,CAAM,QAAA,CAAS,MAAM,IAAA,EAAK;AACxD,EAAA,IAAI,CAAC,KAAA,EAAO;AACV,IAAA,MAAM,IAAI,MAAM,2CAA2C,CAAA;AAAA,EAC7D;AACA,EAAA,MAAM,OAAA,GAAA,iBAAU,IAAI,IAAA,EAAK,EAAE,WAAA,EAAY;AACvC,EAAA,MAAM,IAAA,GAAyB;AAAA,IAC7B,EAAA,EAAI,MAAM,QAAA,CAAS,EAAA;AAAA,IACnB,KAAA;AAAA,IACA,OAAA;AAAA,IACA,UAAU,KAAA,CAAM;AAAA,GAClB;AACA,EAAA,MAAM,MAAA,GAAS,OAAO,OAAA,CAAQ,MAAA,CAAO,OAAK,CAAA,CAAE,EAAA,KAAO,KAAA,CAAM,QAAA,CAAS,EAAE,CAAA;AACpE,EAAA,OAAO,EAAE,GAAG,MAAA,EAAQ,OAAA,EAAS,CAAC,GAAG,MAAA,EAAQ,IAAI,CAAA,EAAE;AACjD;AAEO,SAAS,YAAA,CACd,QACA,EAAA,EACoB;AACpB,EAAA,OAAO,EAAE,GAAG,MAAA,EAAQ,OAAA,EAAS,MAAA,CAAO,OAAA,CAAQ,MAAA,CAAO,CAAA,CAAA,KAAK,CAAA,CAAE,EAAA,KAAO,EAAE,CAAA,EAAE;AACvE;AAEO,SAAS,UAAA,CACd,QACA,EAAA,EAC8B;AAC9B,EAAA,OAAO,OAAO,OAAA,CAAQ,IAAA,CAAK,CAAA,CAAA,KAAK,CAAA,CAAE,OAAO,EAAE,CAAA;AAC7C;AAEA,SAAS,UAAU,MAAA,EAAqC;AACtD,EAAA,IAAI,CAAC,MAAA,IAAU,OAAO,MAAA,KAAW,UAAU,OAAO,KAAA;AAClD,EAAA,MAAM,GAAA,GAAM,MAAA;AACZ,EAAA,MAAM,UAA8B,EAAC;AACrC,EAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,GAAA,CAAI,OAAO,CAAA,EAAG;AAC9B,IAAA,KAAA,MAAW,KAAA,IAAS,IAAI,OAAA,EAAS;AAC/B,MAAA,IAAI,CAAC,KAAA,IAAS,OAAO,KAAA,KAAU,QAAA,EAAU;AACzC,MAAA,MAAM,CAAA,GAAI,KAAA;AACV,MAAA,MAAM,KAAK,OAAO,CAAA,CAAE,EAAA,KAAO,QAAA,GAAW,EAAE,EAAA,GAAK,EAAA;AAC7C,MAAA,MAAM,QAAQ,OAAO,CAAA,CAAE,KAAA,KAAU,QAAA,GAAW,EAAE,KAAA,GAAQ,EAAA;AACtD,MAAA,MAAM,OAAA,GACJ,OAAO,CAAA,CAAE,OAAA,KAAY,QAAA,GAAW,EAAE,OAAA,GAAA,iBAAU,IAAI,IAAA,EAAK,EAAE,WAAA,EAAY;AACrE,MAAA,MAAM,WAAW,CAAA,CAAE,QAAA;AACnB,MAAA,IAAI,CAAC,EAAA,IAAM,CAAC,QAAA,EAAU;AACtB,MAAA,OAAA,CAAQ,KAAK,EAAE,EAAA,EAAI,KAAA,EAAO,OAAA,EAAS,UAAU,CAAA;AAAA,IAC/C;AAAA,EACF;AACA,EAAA,OAAO,EAAE,OAAA,EAAS,qBAAA,EAAuB,OAAA,EAAQ;AACnD","file":"mcp-imports.mjs","sourcesContent":["/**\n * `~/.agentproto/imported-mcps.json` — the user's curated set of\n * discovered MCPs they want the daemon to know about. v1 is just\n * persistence + read-side; the actual MCP-proxy implementation\n * (where the daemon's /mcp endpoint aggregates the imported\n * servers' tools under namespace prefixes) is v2 work.\n *\n * Purpose: today, \"I see you have chrome-devtools in claude\" is\n * read-only. Once a user imports it, the operator agent can refer\n * to it (\"the user said it's enabled — call it\") and a future\n * proxy layer can actually expose it. We persist the choice now so\n * the data is in place when the proxy lands.\n *\n * File shape:\n * {\n * \"version\": 1,\n * \"imports\": [\n * {\n * \"id\": \"claude-code:project:/path:chrome-devtools\",\n * \"alias\": \"chrome-devtools\",\n * \"addedAt\": \"2026-05-10T...\",\n * \"snapshot\": { ...the DiscoveredMcp at import time }\n * }\n * ]\n * }\n *\n * Snapshotting at import time keeps the daemon resilient to the\n * source config getting deleted (user runs `claude mcp remove ...`).\n * The imported entry stays usable; only the next discovery pass\n * stops listing it as \"available to import.\"\n */\n\nimport { promises as fs } from \"node:fs\"\nimport { homedir } from \"node:os\"\nimport { dirname, resolve as resolvePath } from \"node:path\"\nimport type { DiscoveredMcp } from \"./mcp-discovery.js\"\n\nexport const IMPORTED_MCPS_PATH = (): string =>\n resolvePath(homedir(), \".agentproto\", \"imported-mcps.json\")\n\nexport const IMPORTED_MCPS_VERSION = 1 as const\n\nexport interface ImportedMcpEntry {\n id: string\n alias: string\n addedAt: string\n snapshot: DiscoveredMcp\n}\n\nexport interface ImportedMcpsConfig {\n version: typeof IMPORTED_MCPS_VERSION\n imports: ImportedMcpEntry[]\n}\n\nconst EMPTY: ImportedMcpsConfig = {\n version: IMPORTED_MCPS_VERSION,\n imports: [],\n}\n\nexport async function loadImportedMcps(\n path: string = IMPORTED_MCPS_PATH()\n): Promise<ImportedMcpsConfig> {\n let raw: string\n try {\n raw = await fs.readFile(path, \"utf8\")\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return EMPTY\n throw err\n }\n let parsed: unknown\n try {\n parsed = JSON.parse(raw)\n } catch (err) {\n throw new Error(\n `agentproto: ${path} is not valid JSON (${\n err instanceof Error ? err.message : String(err)\n }). Delete or fix manually.`\n )\n }\n return normalize(parsed)\n}\n\nexport async function saveImportedMcps(\n config: ImportedMcpsConfig,\n path: string = IMPORTED_MCPS_PATH()\n): Promise<void> {\n await fs.mkdir(dirname(path), { recursive: true })\n const tmp = `${path}.tmp.${process.pid}`\n await fs.writeFile(tmp, JSON.stringify(config, null, 2) + \"\\n\", \"utf8\")\n await fs.rename(tmp, path)\n}\n\n/**\n * Add (or replace) an import. Snapshots the discovered MCP at\n * import time so the entry stays usable when the source config\n * disappears.\n */\nexport function addImport(\n config: ImportedMcpsConfig,\n input: { snapshot: DiscoveredMcp; alias?: string }\n): ImportedMcpsConfig {\n const alias = (input.alias ?? input.snapshot.name).trim()\n if (!alias) {\n throw new Error(\"addImport: alias resolves to empty string\")\n }\n const addedAt = new Date().toISOString()\n const next: ImportedMcpEntry = {\n id: input.snapshot.id,\n alias,\n addedAt,\n snapshot: input.snapshot,\n }\n const others = config.imports.filter(e => e.id !== input.snapshot.id)\n return { ...config, imports: [...others, next] }\n}\n\nexport function removeImport(\n config: ImportedMcpsConfig,\n id: string\n): ImportedMcpsConfig {\n return { ...config, imports: config.imports.filter(e => e.id !== id) }\n}\n\nexport function findImport(\n config: ImportedMcpsConfig,\n id: string\n): ImportedMcpEntry | undefined {\n return config.imports.find(e => e.id === id)\n}\n\nfunction normalize(parsed: unknown): ImportedMcpsConfig {\n if (!parsed || typeof parsed !== \"object\") return EMPTY\n const obj = parsed as Record<string, unknown>\n const imports: ImportedMcpEntry[] = []\n if (Array.isArray(obj.imports)) {\n for (const entry of obj.imports) {\n if (!entry || typeof entry !== \"object\") continue\n const e = entry as Record<string, unknown>\n const id = typeof e.id === \"string\" ? e.id : \"\"\n const alias = typeof e.alias === \"string\" ? e.alias : id\n const addedAt =\n typeof e.addedAt === \"string\" ? e.addedAt : new Date().toISOString()\n const snapshot = e.snapshot as DiscoveredMcp | undefined\n if (!id || !snapshot) continue\n imports.push({ id, alias, addedAt, snapshot })\n }\n }\n return { version: IMPORTED_MCPS_VERSION, imports }\n}\n"]}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-adapter resume strategies — the daemon's view of "how do I
|
|
3
|
+
* continue a previous session for THIS adapter?".
|
|
4
|
+
*
|
|
5
|
+
* The driver already has an `AgentCliContinuation` declaration in
|
|
6
|
+
* the adapter manifest (AIP-45). That covers strategy NAMES
|
|
7
|
+
* (`native-resume`, `pinned-session`, `transcript`, `none`) but is
|
|
8
|
+
* silent on the mechanics that live OUTSIDE the runtime — like
|
|
9
|
+
* "where on disk does this adapter store its conversations?" or
|
|
10
|
+
* "what argv do I pass to resume into PTY mode?".
|
|
11
|
+
*
|
|
12
|
+
* This table is that practical bridge. Each entry declares three
|
|
13
|
+
* optional hooks:
|
|
14
|
+
*
|
|
15
|
+
* outputHint regex matched against each output line; group 1 is
|
|
16
|
+
* the resume id. Stored as `desc.resumeMetadata[storeAs]`.
|
|
17
|
+
* fsProbe async function that probes a per-adapter directory
|
|
18
|
+
* for a stored session id, scoped to a workspace cwd.
|
|
19
|
+
* Used when `outputHint` never matched.
|
|
20
|
+
* spawnArgs given a captured id, returns the argv to spawn a
|
|
21
|
+
* PTY that resumes the session. agentproto then
|
|
22
|
+
* bypasses ACP/agent-cli and runs the provider
|
|
23
|
+
* directly — the most reliable resume path because
|
|
24
|
+
* it leans on the provider's native UX.
|
|
25
|
+
*
|
|
26
|
+
* Adapters that don't declare anything here fall back to:
|
|
27
|
+
* 1. ACP-level resume via `resumeSessionId` on POST /sessions/agent
|
|
28
|
+
* 2. Fresh spawn (same shape, no continuity)
|
|
29
|
+
*
|
|
30
|
+
* Eventually each adapter should ship its own strategy on
|
|
31
|
+
* `AgentCliHandle.resumeStrategy` (driver-side). This central table
|
|
32
|
+
* is the bootstrap: it lets a new adapter become resume-capable
|
|
33
|
+
* without bumping its npm package; once that lands, entries
|
|
34
|
+
* graduate into the adapter packages.
|
|
35
|
+
*/
|
|
36
|
+
/** Where on disk the resume id ends up on the session descriptor. */
|
|
37
|
+
type ResumeMetadataKey = "claudeResumeId" | "hermesResumeId" | "codexResumeId" | "openClawResumeId" | "openCodeResumeId";
|
|
38
|
+
interface ResumeStrategy {
|
|
39
|
+
/** When set, run on every output line for this adapter. Group 1
|
|
40
|
+
* captures the resume id. The match is saved on the descriptor
|
|
41
|
+
* as `resumeMetadata[storeAs]`. */
|
|
42
|
+
outputHint?: RegExp;
|
|
43
|
+
/** Where the matched id is recorded on the descriptor. */
|
|
44
|
+
storeAs: ResumeMetadataKey;
|
|
45
|
+
/** Probe the adapter's on-disk session store for the most-recently
|
|
46
|
+
* modified session in the given workspace. Returns the resume id
|
|
47
|
+
* or null when nothing eligible. Files older than `prevStartedAt`
|
|
48
|
+
* are ignored (avoids resuming an unrelated prior conversation).
|
|
49
|
+
*
|
|
50
|
+
* Skip when the adapter doesn't persist sessions externally. */
|
|
51
|
+
fsProbe?(cwd: string, prevStartedAt: string): Promise<string | null>;
|
|
52
|
+
/** Return the argv to spawn a PTY that resumes into the given id.
|
|
53
|
+
* When omitted, the daemon falls back to ACP-level resume via
|
|
54
|
+
* the agent-cli protocol instead of the provider's native CLI. */
|
|
55
|
+
spawnArgs?(id: string): string[];
|
|
56
|
+
}
|
|
57
|
+
declare const RESUME_STRATEGIES: Record<string, ResumeStrategy>;
|
|
58
|
+
/**
|
|
59
|
+
* Helper: which adapters declare any resume capability? Used by
|
|
60
|
+
* `agentproto sessions restart` to print a helpful "this adapter
|
|
61
|
+
* doesn't support resume; falling back to fresh spawn" hint when
|
|
62
|
+
* applicable.
|
|
63
|
+
*/
|
|
64
|
+
declare function hasResumeStrategy(adapterSlug: string | undefined): boolean;
|
|
65
|
+
|
|
66
|
+
export { RESUME_STRATEGIES, type ResumeMetadataKey, type ResumeStrategy, hasResumeStrategy };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { promises } from 'fs';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { resolve, join } from 'path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @agentproto/runtime v0.1.0-alpha
|
|
7
|
+
* Long-running gateway: MCP server + HTTP transport + HEARTBEAT autonomy + conversation persistence over a workspace dir.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
var RESUME_STRATEGIES = {
|
|
11
|
+
"claude-code": {
|
|
12
|
+
// Printed by claude on graceful exit when session persistence
|
|
13
|
+
// is on (default). Example: `claude --resume 0e483f81-1a44-4bec-9667-b37158450296`
|
|
14
|
+
outputHint: /claude\s+--resume\s+([0-9a-f-]{8,})/i,
|
|
15
|
+
storeAs: "claudeResumeId",
|
|
16
|
+
fsProbe: probeMtimeLatestJsonl(".claude/projects"),
|
|
17
|
+
spawnArgs: (id) => ["claude", "--resume", id]
|
|
18
|
+
}
|
|
19
|
+
// Stubs for other shipped adapters — fill in as we learn each
|
|
20
|
+
// provider's resume mechanism. Today they fall back to ACP-level
|
|
21
|
+
// resume (whatever the agent-cli runtime supports) or fresh spawn.
|
|
22
|
+
//
|
|
23
|
+
// hermes: { storeAs: "hermesResumeId", ... }
|
|
24
|
+
// codex: { storeAs: "codexResumeId", ... }
|
|
25
|
+
// openclaw: { storeAs: "openClawResumeId", ... }
|
|
26
|
+
// opencode: { storeAs: "openCodeResumeId", ... }
|
|
27
|
+
};
|
|
28
|
+
function probeMtimeLatestJsonl(storeRel) {
|
|
29
|
+
return async (cwd, prevStartedAt) => {
|
|
30
|
+
const encoded = cwd.replace(/\//g, "-");
|
|
31
|
+
const dir = resolve(homedir(), storeRel, encoded);
|
|
32
|
+
let entries;
|
|
33
|
+
try {
|
|
34
|
+
entries = await promises.readdir(dir);
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const jsonl = entries.filter((e) => e.endsWith(".jsonl"));
|
|
39
|
+
if (jsonl.length === 0) return null;
|
|
40
|
+
const startedAtMs = Date.parse(prevStartedAt);
|
|
41
|
+
const candidates = [];
|
|
42
|
+
for (const f of jsonl) {
|
|
43
|
+
try {
|
|
44
|
+
const st = await promises.stat(join(dir, f));
|
|
45
|
+
if (!Number.isFinite(startedAtMs) || st.mtimeMs >= startedAtMs - 1e3) {
|
|
46
|
+
candidates.push({ name: f, mtime: st.mtimeMs });
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (candidates.length === 0) return null;
|
|
52
|
+
candidates.sort((a, b) => b.mtime - a.mtime);
|
|
53
|
+
return candidates[0].name.replace(/\.jsonl$/, "");
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function hasResumeStrategy(adapterSlug) {
|
|
57
|
+
if (!adapterSlug) return false;
|
|
58
|
+
const s = RESUME_STRATEGIES[adapterSlug];
|
|
59
|
+
return !!(s && (s.outputHint || s.fsProbe || s.spawnArgs));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { RESUME_STRATEGIES, hasResumeStrategy };
|
|
63
|
+
//# sourceMappingURL=resume-strategies.mjs.map
|
|
64
|
+
//# sourceMappingURL=resume-strategies.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/resume-strategies.ts"],"names":["fs"],"mappings":";;;;;;;;;AAoEO,IAAM,iBAAA,GAAoD;AAAA,EAC/D,aAAA,EAAe;AAAA;AAAA;AAAA,IAGb,UAAA,EAAY,sCAAA;AAAA,IACZ,OAAA,EAAS,gBAAA;AAAA,IACT,OAAA,EAAS,sBAAsB,kBAAkB,CAAA;AAAA,IACjD,SAAA,EAAW,CAAA,EAAA,KAAM,CAAC,QAAA,EAAU,YAAY,EAAE;AAAA;AAC5C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUF;AASA,SAAS,sBACP,QAAA,EACgE;AAChE,EAAA,OAAO,OAAO,KAAK,aAAA,KAAkB;AACnC,IAAA,MAAM,OAAA,GAAU,GAAA,CAAI,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA;AACtC,IAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,OAAA,EAAQ,EAAG,UAAU,OAAO,CAAA;AAChD,IAAA,IAAI,OAAA;AACJ,IAAA,IAAI;AACF,MAAA,OAAA,GAAU,MAAMA,QAAA,CAAG,OAAA,CAAQ,GAAG,CAAA;AAAA,IAChC,CAAA,CAAA,MAAQ;AACN,MAAA,OAAO,IAAA;AAAA,IACT;AACA,IAAA,MAAM,QAAQ,OAAA,CAAQ,MAAA,CAAO,OAAK,CAAA,CAAE,QAAA,CAAS,QAAQ,CAAC,CAAA;AACtD,IAAA,IAAI,KAAA,CAAM,MAAA,KAAW,CAAA,EAAG,OAAO,IAAA;AAC/B,IAAA,MAAM,WAAA,GAAc,IAAA,CAAK,KAAA,CAAM,aAAa,CAAA;AAC5C,IAAA,MAAM,aAAgD,EAAC;AACvD,IAAA,KAAA,MAAW,KAAK,KAAA,EAAO;AACrB,MAAA,IAAI;AACF,QAAA,MAAM,KAAK,MAAMA,QAAA,CAAG,KAAK,IAAA,CAAK,GAAA,EAAK,CAAC,CAAC,CAAA;AACrC,QAAA,IAAI,CAAC,OAAO,QAAA,CAAS,WAAW,KAAK,EAAA,CAAG,OAAA,IAAW,cAAc,GAAA,EAAM;AACrE,UAAA,UAAA,CAAW,KAAK,EAAE,IAAA,EAAM,GAAG,KAAA,EAAO,EAAA,CAAG,SAAS,CAAA;AAAA,QAChD;AAAA,MACF,CAAA,CAAA,MAAQ;AAAA,MAER;AAAA,IACF;AACA,IAAA,IAAI,UAAA,CAAW,MAAA,KAAW,CAAA,EAAG,OAAO,IAAA;AACpC,IAAA,UAAA,CAAW,KAAK,CAAC,CAAA,EAAG,MAAM,CAAA,CAAE,KAAA,GAAQ,EAAE,KAAK,CAAA;AAC3C,IAAA,OAAO,WAAW,CAAC,CAAA,CAAG,IAAA,CAAK,OAAA,CAAQ,YAAY,EAAE,CAAA;AAAA,EACnD,CAAA;AACF;AAQO,SAAS,kBAAkB,WAAA,EAA0C;AAC1E,EAAA,IAAI,CAAC,aAAa,OAAO,KAAA;AACzB,EAAA,MAAM,CAAA,GAAI,kBAAkB,WAAW,CAAA;AACvC,EAAA,OAAO,CAAC,EAAE,CAAA,KAAM,EAAE,UAAA,IAAc,CAAA,CAAE,WAAW,CAAA,CAAE,SAAA,CAAA,CAAA;AACjD","file":"resume-strategies.mjs","sourcesContent":["/**\n * Per-adapter resume strategies — the daemon's view of \"how do I\n * continue a previous session for THIS adapter?\".\n *\n * The driver already has an `AgentCliContinuation` declaration in\n * the adapter manifest (AIP-45). That covers strategy NAMES\n * (`native-resume`, `pinned-session`, `transcript`, `none`) but is\n * silent on the mechanics that live OUTSIDE the runtime — like\n * \"where on disk does this adapter store its conversations?\" or\n * \"what argv do I pass to resume into PTY mode?\".\n *\n * This table is that practical bridge. Each entry declares three\n * optional hooks:\n *\n * outputHint regex matched against each output line; group 1 is\n * the resume id. Stored as `desc.resumeMetadata[storeAs]`.\n * fsProbe async function that probes a per-adapter directory\n * for a stored session id, scoped to a workspace cwd.\n * Used when `outputHint` never matched.\n * spawnArgs given a captured id, returns the argv to spawn a\n * PTY that resumes the session. agentproto then\n * bypasses ACP/agent-cli and runs the provider\n * directly — the most reliable resume path because\n * it leans on the provider's native UX.\n *\n * Adapters that don't declare anything here fall back to:\n * 1. ACP-level resume via `resumeSessionId` on POST /sessions/agent\n * 2. Fresh spawn (same shape, no continuity)\n *\n * Eventually each adapter should ship its own strategy on\n * `AgentCliHandle.resumeStrategy` (driver-side). This central table\n * is the bootstrap: it lets a new adapter become resume-capable\n * without bumping its npm package; once that lands, entries\n * graduate into the adapter packages.\n */\n\nimport { promises as fs } from \"node:fs\"\nimport { homedir } from \"node:os\"\nimport { join, resolve } from \"node:path\"\n\n/** Where on disk the resume id ends up on the session descriptor. */\nexport type ResumeMetadataKey =\n | \"claudeResumeId\"\n | \"hermesResumeId\"\n | \"codexResumeId\"\n | \"openClawResumeId\"\n | \"openCodeResumeId\"\n\nexport interface ResumeStrategy {\n /** When set, run on every output line for this adapter. Group 1\n * captures the resume id. The match is saved on the descriptor\n * as `resumeMetadata[storeAs]`. */\n outputHint?: RegExp\n /** Where the matched id is recorded on the descriptor. */\n storeAs: ResumeMetadataKey\n /** Probe the adapter's on-disk session store for the most-recently\n * modified session in the given workspace. Returns the resume id\n * or null when nothing eligible. Files older than `prevStartedAt`\n * are ignored (avoids resuming an unrelated prior conversation).\n *\n * Skip when the adapter doesn't persist sessions externally. */\n fsProbe?(cwd: string, prevStartedAt: string): Promise<string | null>\n /** Return the argv to spawn a PTY that resumes into the given id.\n * When omitted, the daemon falls back to ACP-level resume via\n * the agent-cli protocol instead of the provider's native CLI. */\n spawnArgs?(id: string): string[]\n}\n\nexport const RESUME_STRATEGIES: Record<string, ResumeStrategy> = {\n \"claude-code\": {\n // Printed by claude on graceful exit when session persistence\n // is on (default). Example: `claude --resume 0e483f81-1a44-4bec-9667-b37158450296`\n outputHint: /claude\\s+--resume\\s+([0-9a-f-]{8,})/i,\n storeAs: \"claudeResumeId\",\n fsProbe: probeMtimeLatestJsonl(\".claude/projects\"),\n spawnArgs: id => [\"claude\", \"--resume\", id],\n },\n\n // Stubs for other shipped adapters — fill in as we learn each\n // provider's resume mechanism. Today they fall back to ACP-level\n // resume (whatever the agent-cli runtime supports) or fresh spawn.\n //\n // hermes: { storeAs: \"hermesResumeId\", ... }\n // codex: { storeAs: \"codexResumeId\", ... }\n // openclaw: { storeAs: \"openClawResumeId\", ... }\n // opencode: { storeAs: \"openCodeResumeId\", ... }\n}\n\n/**\n * Build an `fsProbe` for adapters that store one `.jsonl` per session\n * inside `~/<storeRel>/<cwd-encoded>/`. The encoded cwd is the\n * absolute path with `/` → `-` (claude's convention; others tend to\n * use the same scheme). Returns the UUID of the most-recently\n * modified `.jsonl`, filtered to files at-or-after `prevStartedAt`.\n */\nfunction probeMtimeLatestJsonl(\n storeRel: string,\n): (cwd: string, prevStartedAt: string) => Promise<string | null> {\n return async (cwd, prevStartedAt) => {\n const encoded = cwd.replace(/\\//g, \"-\")\n const dir = resolve(homedir(), storeRel, encoded)\n let entries: string[]\n try {\n entries = await fs.readdir(dir)\n } catch {\n return null\n }\n const jsonl = entries.filter(e => e.endsWith(\".jsonl\"))\n if (jsonl.length === 0) return null\n const startedAtMs = Date.parse(prevStartedAt)\n const candidates: { name: string; mtime: number }[] = []\n for (const f of jsonl) {\n try {\n const st = await fs.stat(join(dir, f))\n if (!Number.isFinite(startedAtMs) || st.mtimeMs >= startedAtMs - 1000) {\n candidates.push({ name: f, mtime: st.mtimeMs })\n }\n } catch {\n // ignore unreadable entry\n }\n }\n if (candidates.length === 0) return null\n candidates.sort((a, b) => b.mtime - a.mtime)\n return candidates[0]!.name.replace(/\\.jsonl$/, \"\")\n }\n}\n\n/**\n * Helper: which adapters declare any resume capability? Used by\n * `agentproto sessions restart` to print a helpful \"this adapter\n * doesn't support resume; falling back to fresh spawn\" hint when\n * applicable.\n */\nexport function hasResumeStrategy(adapterSlug: string | undefined): boolean {\n if (!adapterSlug) return false\n const s = RESUME_STRATEGIES[adapterSlug]\n return !!(s && (s.outputHint || s.fsProbe || s.spawnArgs))\n}\n"]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `WorkspaceFs` — minimal `readFile` / `writeFile` adapter over a
|
|
3
|
+
* workspace directory. Mirrors the `McpWorkspace.filesystem` shape
|
|
4
|
+
* used by `@guilde/mcp` so the daemon can be plugged in as the
|
|
5
|
+
* workspace backend for a Guilde MCP server (via its
|
|
6
|
+
* `loadGuildWorkspace` injection point) without re-shimming.
|
|
7
|
+
*
|
|
8
|
+
* All paths are workspace-relative. Absolute paths are rejected to
|
|
9
|
+
* keep the surface predictable when the daemon binds to a public
|
|
10
|
+
* port.
|
|
11
|
+
*/
|
|
12
|
+
interface WorkspaceFs {
|
|
13
|
+
readFile(path: string): Promise<string>;
|
|
14
|
+
writeFile(path: string, content: string | Uint8Array): Promise<void>;
|
|
15
|
+
exists(path: string): Promise<boolean>;
|
|
16
|
+
}
|
|
17
|
+
interface CreateWorkspaceFsOptions {
|
|
18
|
+
/** Absolute path to the workspace root. */
|
|
19
|
+
workspace: string;
|
|
20
|
+
}
|
|
21
|
+
declare class WorkspacePathError extends Error {
|
|
22
|
+
constructor(message: string);
|
|
23
|
+
}
|
|
24
|
+
declare function createWorkspaceFs(opts: CreateWorkspaceFsOptions): WorkspaceFs;
|
|
25
|
+
|
|
26
|
+
export { type CreateWorkspaceFsOptions, type WorkspaceFs, WorkspacePathError, createWorkspaceFs };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { mkdir, writeFile, readFile } from 'fs/promises';
|
|
3
|
+
import { resolve, dirname, isAbsolute, normalize, join, relative } from 'path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @agentproto/runtime v0.1.0-alpha
|
|
7
|
+
* Long-running gateway: MCP server + HTTP transport + HEARTBEAT autonomy + conversation persistence over a workspace dir.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
var WorkspacePathError = class extends Error {
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "WorkspacePathError";
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
function createWorkspaceFs(opts) {
|
|
17
|
+
const root = resolve(opts.workspace);
|
|
18
|
+
function resolvePath(path) {
|
|
19
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
20
|
+
throw new WorkspacePathError("path must be a non-empty string");
|
|
21
|
+
}
|
|
22
|
+
if (isAbsolute(path)) {
|
|
23
|
+
throw new WorkspacePathError(
|
|
24
|
+
"absolute paths are not allowed; use a workspace-relative path"
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
const joined = normalize(join(root, path));
|
|
28
|
+
const rel = relative(root, joined);
|
|
29
|
+
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
30
|
+
throw new WorkspacePathError(
|
|
31
|
+
`path escapes the workspace: '${path}'`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return joined;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
async readFile(path) {
|
|
38
|
+
const abs = resolvePath(path);
|
|
39
|
+
const buf = await readFile(abs);
|
|
40
|
+
return buf.toString("utf8");
|
|
41
|
+
},
|
|
42
|
+
async writeFile(path, content) {
|
|
43
|
+
const abs = resolvePath(path);
|
|
44
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
45
|
+
await writeFile(abs, content);
|
|
46
|
+
},
|
|
47
|
+
async exists(path) {
|
|
48
|
+
try {
|
|
49
|
+
const abs = resolvePath(path);
|
|
50
|
+
return existsSync(abs);
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { WorkspacePathError, createWorkspaceFs };
|
|
59
|
+
//# sourceMappingURL=workspace-fs.mjs.map
|
|
60
|
+
//# sourceMappingURL=workspace-fs.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/workspace-fs.ts"],"names":["fsReadFile","fsWriteFile"],"mappings":";;;;;;;;;AA2BO,IAAM,kBAAA,GAAN,cAAiC,KAAA,CAAM;AAAA,EAC5C,YAAY,OAAA,EAAiB;AAC3B,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,oBAAA;AAAA,EACd;AACF;AAEO,SAAS,kBAAkB,IAAA,EAA6C;AAC7E,EAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,IAAA,CAAK,SAAS,CAAA;AAEnC,EAAA,SAAS,YAAY,IAAA,EAAsB;AACzC,IAAA,IAAI,OAAO,IAAA,KAAS,QAAA,IAAY,IAAA,CAAK,WAAW,CAAA,EAAG;AACjD,MAAA,MAAM,IAAI,mBAAmB,iCAAiC,CAAA;AAAA,IAChE;AACA,IAAA,IAAI,UAAA,CAAW,IAAI,CAAA,EAAG;AACpB,MAAA,MAAM,IAAI,kBAAA;AAAA,QACR;AAAA,OACF;AAAA,IACF;AACA,IAAA,MAAM,MAAA,GAAS,SAAA,CAAU,IAAA,CAAK,IAAA,EAAM,IAAI,CAAC,CAAA;AACzC,IAAA,MAAM,GAAA,GAAM,QAAA,CAAS,IAAA,EAAM,MAAM,CAAA;AACjC,IAAA,IAAI,IAAI,UAAA,CAAW,IAAI,CAAA,IAAK,UAAA,CAAW,GAAG,CAAA,EAAG;AAC3C,MAAA,MAAM,IAAI,kBAAA;AAAA,QACR,gCAAgC,IAAI,CAAA,CAAA;AAAA,OACtC;AAAA,IACF;AACA,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,OAAO;AAAA,IACL,MAAM,SAAS,IAAA,EAAM;AACnB,MAAA,MAAM,GAAA,GAAM,YAAY,IAAI,CAAA;AAC5B,MAAA,MAAM,GAAA,GAAM,MAAMA,QAAA,CAAW,GAAG,CAAA;AAChC,MAAA,OAAO,GAAA,CAAI,SAAS,MAAM,CAAA;AAAA,IAC5B,CAAA;AAAA,IACA,MAAM,SAAA,CAAU,IAAA,EAAM,OAAA,EAAS;AAC7B,MAAA,MAAM,GAAA,GAAM,YAAY,IAAI,CAAA;AAC5B,MAAA,MAAM,MAAM,OAAA,CAAQ,GAAG,GAAG,EAAE,SAAA,EAAW,MAAM,CAAA;AAC7C,MAAA,MAAMC,SAAA,CAAY,KAAK,OAAO,CAAA;AAAA,IAChC,CAAA;AAAA,IACA,MAAM,OAAO,IAAA,EAAM;AACjB,MAAA,IAAI;AACF,QAAA,MAAM,GAAA,GAAM,YAAY,IAAI,CAAA;AAC5B,QAAA,OAAO,WAAW,GAAG,CAAA;AAAA,MACvB,CAAA,CAAA,MAAQ;AACN,QAAA,OAAO,KAAA;AAAA,MACT;AAAA,IACF;AAAA,GACF;AACF","file":"workspace-fs.mjs","sourcesContent":["/**\n * `WorkspaceFs` — minimal `readFile` / `writeFile` adapter over a\n * workspace directory. Mirrors the `McpWorkspace.filesystem` shape\n * used by `@guilde/mcp` so the daemon can be plugged in as the\n * workspace backend for a Guilde MCP server (via its\n * `loadGuildWorkspace` injection point) without re-shimming.\n *\n * All paths are workspace-relative. Absolute paths are rejected to\n * keep the surface predictable when the daemon binds to a public\n * port.\n */\n\nimport { existsSync } from \"node:fs\"\nimport { mkdir, readFile as fsReadFile, writeFile as fsWriteFile } from \"node:fs/promises\"\nimport { dirname, isAbsolute, join, normalize, relative, resolve } from \"node:path\"\n\nexport interface WorkspaceFs {\n readFile(path: string): Promise<string>\n writeFile(path: string, content: string | Uint8Array): Promise<void>\n exists(path: string): Promise<boolean>\n}\n\nexport interface CreateWorkspaceFsOptions {\n /** Absolute path to the workspace root. */\n workspace: string\n}\n\nexport class WorkspacePathError extends Error {\n constructor(message: string) {\n super(message)\n this.name = \"WorkspacePathError\"\n }\n}\n\nexport function createWorkspaceFs(opts: CreateWorkspaceFsOptions): WorkspaceFs {\n const root = resolve(opts.workspace)\n\n function resolvePath(path: string): string {\n if (typeof path !== \"string\" || path.length === 0) {\n throw new WorkspacePathError(\"path must be a non-empty string\")\n }\n if (isAbsolute(path)) {\n throw new WorkspacePathError(\n \"absolute paths are not allowed; use a workspace-relative path\",\n )\n }\n const joined = normalize(join(root, path))\n const rel = relative(root, joined)\n if (rel.startsWith(\"..\") || isAbsolute(rel)) {\n throw new WorkspacePathError(\n `path escapes the workspace: '${path}'`,\n )\n }\n return joined\n }\n\n return {\n async readFile(path) {\n const abs = resolvePath(path)\n const buf = await fsReadFile(abs)\n return buf.toString(\"utf8\")\n },\n async writeFile(path, content) {\n const abs = resolvePath(path)\n await mkdir(dirname(abs), { recursive: true })\n await fsWriteFile(abs, content)\n },\n async exists(path) {\n try {\n const abs = resolvePath(path)\n return existsSync(abs)\n } catch {\n return false\n }\n },\n }\n}\n"]}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `~/.agentproto/workspaces.json` — single source of truth for the
|
|
3
|
+
* local agentproto control plane. Tracks which directories the user
|
|
4
|
+
* has opted into as agentproto workspaces, plus which one is "active"
|
|
5
|
+
* (the daemon's default working set when no workspace is named in
|
|
6
|
+
* a tool call).
|
|
7
|
+
*
|
|
8
|
+
* Why a config file instead of recreating an MCP entry per workspace:
|
|
9
|
+
* - One daemon = one MCP entry in the user's IDE (clean to read in
|
|
10
|
+
* `claude mcp list`, no `dapper_willow` / `noble_lantern` spam).
|
|
11
|
+
* - The daemon walks `workspaces[]` at boot and exposes them all;
|
|
12
|
+
* tools that need a target take an optional `workspace` arg and
|
|
13
|
+
* fall back to `active` when omitted.
|
|
14
|
+
* - The CLI (`agentproto workspace add/list/remove/use`), the daemon,
|
|
15
|
+
* and the guilde-web onboarding dialog all read+write the same
|
|
16
|
+
* file, so changes from one surface are immediately visible to
|
|
17
|
+
* the others.
|
|
18
|
+
*
|
|
19
|
+
* File layout is intentionally boring (small, hand-editable JSON) —
|
|
20
|
+
* future fields go on top, never break v1 readers.
|
|
21
|
+
*/
|
|
22
|
+
declare const WORKSPACES_CONFIG_VERSION: 1;
|
|
23
|
+
interface WorkspaceEntry {
|
|
24
|
+
/** Stable handle — what tools accept as `workspace`. Lowercase
|
|
25
|
+
* ASCII, hyphen-only. The CLI sanitises arbitrary names down to
|
|
26
|
+
* this shape so paste-from-finder Just Works. */
|
|
27
|
+
slug: string;
|
|
28
|
+
/** Absolute filesystem path. Required absolute so the daemon can
|
|
29
|
+
* serve the workspace regardless of its own cwd. */
|
|
30
|
+
path: string;
|
|
31
|
+
/** ISO-8601 — first time the entry was added. Pure metadata, no
|
|
32
|
+
* behavioural impact. */
|
|
33
|
+
addedAt: string;
|
|
34
|
+
/** Last time the entry was touched (renamed, path edited, used as
|
|
35
|
+
* active). Lets the CLI sort by recency without an N+1 stat. */
|
|
36
|
+
updatedAt: string;
|
|
37
|
+
/** Free-text label the user can attach so workspaces with similar
|
|
38
|
+
* slugs stay distinguishable in `agentproto workspace list`. */
|
|
39
|
+
label?: string;
|
|
40
|
+
}
|
|
41
|
+
interface WorkspacesConfig {
|
|
42
|
+
version: typeof WORKSPACES_CONFIG_VERSION;
|
|
43
|
+
/** Active workspace slug — daemon defaults to this when no
|
|
44
|
+
* `workspace` arg is passed. May be undefined when no workspaces
|
|
45
|
+
* are registered yet. */
|
|
46
|
+
active?: string;
|
|
47
|
+
workspaces: WorkspaceEntry[];
|
|
48
|
+
}
|
|
49
|
+
declare const DEFAULT_CONFIG_DIR: () => string;
|
|
50
|
+
declare const DEFAULT_CONFIG_PATH: () => string;
|
|
51
|
+
/** Reduce arbitrary user input to a safe slug. Aligns with what most
|
|
52
|
+
* tooling validates against (`/^[a-z0-9][a-z0-9-]{0,63}$/`) so the
|
|
53
|
+
* daemon never has to encode it for URLs or filesystem paths. */
|
|
54
|
+
declare function sanitizeSlug(input: string): string;
|
|
55
|
+
/** Read the config from disk. Returns an empty config (no workspaces,
|
|
56
|
+
* no active) when the file is missing — this is the legitimate
|
|
57
|
+
* first-boot state, not an error. */
|
|
58
|
+
declare function loadWorkspacesConfig(path?: string): Promise<WorkspacesConfig>;
|
|
59
|
+
/** Write the config back. Creates the parent directory if missing.
|
|
60
|
+
* Atomic-ish: writes to a temp file first, then renames over the
|
|
61
|
+
* target — avoids the partial-write window where a concurrent reader
|
|
62
|
+
* sees half a JSON object. */
|
|
63
|
+
declare function saveWorkspacesConfig(config: WorkspacesConfig, path?: string): Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* Pure helpers — caller manages persistence. Returns a NEW config
|
|
66
|
+
* (never mutates) so callers can compose multiple updates before
|
|
67
|
+
* a single save.
|
|
68
|
+
*/
|
|
69
|
+
declare function addWorkspace(config: WorkspacesConfig, input: {
|
|
70
|
+
slug: string;
|
|
71
|
+
path: string;
|
|
72
|
+
label?: string;
|
|
73
|
+
}): WorkspacesConfig;
|
|
74
|
+
declare function removeWorkspace(config: WorkspacesConfig, slug: string): WorkspacesConfig;
|
|
75
|
+
declare function setActiveWorkspace(config: WorkspacesConfig, slug: string): WorkspacesConfig;
|
|
76
|
+
declare function findWorkspace(config: WorkspacesConfig, slug: string): WorkspaceEntry | undefined;
|
|
77
|
+
/** The active workspace, or undefined when none is registered. Pure
|
|
78
|
+
* convenience to avoid re-implementing the lookup at every caller. */
|
|
79
|
+
declare function getActiveWorkspace(config: WorkspacesConfig): WorkspaceEntry | undefined;
|
|
80
|
+
|
|
81
|
+
export { DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_PATH, WORKSPACES_CONFIG_VERSION, type WorkspaceEntry, type WorkspacesConfig, addWorkspace, findWorkspace, getActiveWorkspace, loadWorkspacesConfig, removeWorkspace, sanitizeSlug, saveWorkspacesConfig, setActiveWorkspace };
|