@agentproto/runtime 1.0.0 → 2.0.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 +3 -1
- package/dist/catalog-models.d.ts +255 -0
- package/dist/catalog-models.mjs +386 -0
- package/dist/catalog-models.mjs.map +1 -0
- package/dist/config.d.ts +378 -1
- package/dist/config.mjs.map +1 -1
- package/dist/context-continuity-B9n0t0v-.d.ts +53 -0
- package/dist/index.d.ts +5377 -2160
- package/dist/index.mjs +21719 -11064
- package/dist/index.mjs.map +1 -1
- package/dist/pr-provenance.d.ts +140 -0
- package/dist/pr-provenance.mjs +106 -0
- package/dist/pr-provenance.mjs.map +1 -0
- package/dist/resume-strategies.d.ts +39 -2
- package/dist/resume-strategies.mjs +765 -28
- package/dist/resume-strategies.mjs.map +1 -1
- package/dist/session-config-DIf6wYYP.d.ts +192 -0
- package/dist/session-story-panel.d.ts +26 -0
- package/dist/session-story-panel.mjs +970 -0
- package/dist/session-story-panel.mjs.map +1 -0
- package/dist/session-story.d.ts +14 -2
- package/dist/spawn-defaults-7uHRnYH1.d.ts +414 -0
- package/dist/telegram-proxy.d.ts +35 -0
- package/dist/telegram-proxy.mjs +115 -0
- package/dist/telegram-proxy.mjs.map +1 -0
- package/dist/user-presets.d.ts +91 -0
- package/dist/user-presets.mjs +84 -0
- package/dist/user-presets.mjs.map +1 -0
- package/dist/workspaces-config.mjs +13 -3
- package/dist/workspaces-config.mjs.map +1 -1
- package/package.json +48 -23
- package/dist/config-BRKy_SAF.d.ts +0 -569
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { createServer, request } from 'http';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @agentproto/runtime v0.1.0-alpha
|
|
5
|
+
* Long-running gateway: MCP server + HTTP transport + HEARTBEAT autonomy + conversation persistence over a workspace dir.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function createTelegramProxy(opts) {
|
|
9
|
+
const log = opts.log ?? (() => {
|
|
10
|
+
});
|
|
11
|
+
let server = null;
|
|
12
|
+
return {
|
|
13
|
+
async start() {
|
|
14
|
+
server = createServer((req, res) => {
|
|
15
|
+
handleProxyRequest(req, res, opts, log);
|
|
16
|
+
});
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
server.once("error", reject);
|
|
19
|
+
server.listen(opts.listenPort, "127.0.0.1", () => {
|
|
20
|
+
const addr = server.address();
|
|
21
|
+
const port = typeof addr === "object" && addr !== null ? addr.port : opts.listenPort;
|
|
22
|
+
const url = `http://127.0.0.1:${port}`;
|
|
23
|
+
log(`[telegram-proxy] listening on ${url}`);
|
|
24
|
+
resolve({ url, port });
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
},
|
|
28
|
+
async stop() {
|
|
29
|
+
if (!server) return;
|
|
30
|
+
await new Promise((resolve, reject) => {
|
|
31
|
+
server.close((err) => {
|
|
32
|
+
if (err) reject(err);
|
|
33
|
+
else resolve();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
server = null;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function handleProxyRequest(req, res, opts, log) {
|
|
41
|
+
if (req.method !== "POST") {
|
|
42
|
+
res.writeHead(405, { "content-type": "application/json" });
|
|
43
|
+
res.end(JSON.stringify({ error: "method_not_allowed" }));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const rawUrl = req.url ?? "/";
|
|
47
|
+
const normalized = normalizePath(rawUrl);
|
|
48
|
+
if (!normalized.startsWith("/inbound/")) {
|
|
49
|
+
res.writeHead(404, { "content-type": "application/json" });
|
|
50
|
+
res.end(JSON.stringify({ error: "not_found" }));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const targetUrl = new URL(normalized, opts.targetBaseUrl);
|
|
54
|
+
const queryIdx = rawUrl.indexOf("?");
|
|
55
|
+
if (queryIdx >= 0) {
|
|
56
|
+
targetUrl.search = rawUrl.slice(queryIdx);
|
|
57
|
+
}
|
|
58
|
+
log(`[telegram-proxy] ${req.method} ${rawUrl} -> ${targetUrl.toString()}`);
|
|
59
|
+
const headers = {};
|
|
60
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
61
|
+
if (value !== void 0) {
|
|
62
|
+
headers[key] = value;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
headers.host = targetUrl.host;
|
|
66
|
+
const proxyReq = request(
|
|
67
|
+
{
|
|
68
|
+
hostname: targetUrl.hostname,
|
|
69
|
+
port: targetUrl.port || (targetUrl.protocol === "https:" ? 443 : 80),
|
|
70
|
+
path: targetUrl.pathname + targetUrl.search,
|
|
71
|
+
method: req.method,
|
|
72
|
+
headers
|
|
73
|
+
},
|
|
74
|
+
(proxyRes) => {
|
|
75
|
+
res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);
|
|
76
|
+
proxyRes.pipe(res);
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
proxyReq.on("error", (err) => {
|
|
80
|
+
log(`[telegram-proxy] upstream error: ${err.message}`);
|
|
81
|
+
if (!res.headersSent) {
|
|
82
|
+
res.writeHead(502, { "content-type": "application/json" });
|
|
83
|
+
res.end(JSON.stringify({ error: "bad_gateway" }));
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
req.pipe(proxyReq);
|
|
87
|
+
}
|
|
88
|
+
function normalizePath(urlPath) {
|
|
89
|
+
const pathPart = urlPath.split("?")[0] ?? urlPath;
|
|
90
|
+
let decoded;
|
|
91
|
+
try {
|
|
92
|
+
decoded = decodeURIComponent(pathPart);
|
|
93
|
+
} catch {
|
|
94
|
+
return pathPart;
|
|
95
|
+
}
|
|
96
|
+
const hasTrailingSlash = decoded.endsWith("/") && decoded !== "/";
|
|
97
|
+
const segments = decoded.split("/").filter((s) => s !== "" && s !== ".");
|
|
98
|
+
const normalized = [];
|
|
99
|
+
for (const seg of segments) {
|
|
100
|
+
if (seg === "..") {
|
|
101
|
+
normalized.pop();
|
|
102
|
+
} else {
|
|
103
|
+
normalized.push(seg);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
let result = "/" + normalized.join("/");
|
|
107
|
+
if (hasTrailingSlash && !result.endsWith("/")) {
|
|
108
|
+
result += "/";
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export { createTelegramProxy };
|
|
114
|
+
//# sourceMappingURL=telegram-proxy.mjs.map
|
|
115
|
+
//# sourceMappingURL=telegram-proxy.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/telegram-proxy.ts"],"names":[],"mappings":"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","file":"telegram-proxy.mjs","sourcesContent":["/**\n * Narrow public ingress proxy for Telegram (and other) bot webhooks.\n *\n * SECURITY: the daemon's port (default 18790) exposes the entire control\n * plane — `session_list`, `agent_start`, `agent_kill`, etc. A bare public\n * tunnel to that port would expose full daemon control to anyone who finds\n * the URL. This proxy listens on its own port and forwards ONLY\n * `POST /inbound/*` to the daemon. Every other path or method is rejected\n * with 404/405.\n */\n\nimport { createServer, request, type IncomingMessage, type Server, type ServerResponse } from \"node:http\"\n\nexport interface TelegramProxyOptions {\n targetBaseUrl: string\n listenPort: number\n log?: (msg: string) => void\n}\n\nexport interface TelegramProxy {\n start(): Promise<{ url: string; port: number }>\n stop(): Promise<void>\n}\n\n/**\n * Create a narrow reverse-proxy that forwards ONLY `POST /inbound/*`\n * to a target base URL. Every other path or method is rejected.\n *\n * SECURITY: the URL path is decoded, normalized (resolving `.` and `..`),\n * and then checked. Only paths whose normalized form literally begins with\n * `/inbound/` are forwarded. Path-traversal attempts such as\n * `/inbound/../sessions/foo` decode and normalize to `/sessions/foo`, which\n * does not start with `/inbound/` and is rejected with 404.\n */\nexport function createTelegramProxy(opts: TelegramProxyOptions): TelegramProxy {\n const log = opts.log ?? (() => {})\n let server: Server | null = null\n\n return {\n async start() {\n server = createServer((req, res) => {\n handleProxyRequest(req, res, opts, log)\n })\n\n return new Promise<{ url: string; port: number }>((resolve, reject) => {\n server!.once(\"error\", reject)\n server!.listen(opts.listenPort, \"127.0.0.1\", () => {\n const addr = server!.address()\n const port = typeof addr === \"object\" && addr !== null ? addr.port : opts.listenPort\n const url = `http://127.0.0.1:${port}`\n log(`[telegram-proxy] listening on ${url}`)\n resolve({ url, port })\n })\n })\n },\n\n async stop() {\n if (!server) return\n await new Promise<void>((resolve, reject) => {\n server!.close(err => {\n if (err) reject(err)\n else resolve()\n })\n })\n server = null\n },\n }\n}\n\nfunction handleProxyRequest(\n req: IncomingMessage,\n res: ServerResponse,\n opts: TelegramProxyOptions,\n log: (msg: string) => void,\n): void {\n // Only POST is allowed.\n if (req.method !== \"POST\") {\n res.writeHead(405, { \"content-type\": \"application/json\" })\n res.end(JSON.stringify({ error: \"method_not_allowed\" }))\n return\n }\n\n const rawUrl = req.url ?? \"/\"\n const normalized = normalizePath(rawUrl)\n\n // SECURITY: only forward paths whose normalized form literally begins with /inbound/\n if (!normalized.startsWith(\"/inbound/\")) {\n res.writeHead(404, { \"content-type\": \"application/json\" })\n res.end(JSON.stringify({ error: \"not_found\" }))\n return\n }\n\n const targetUrl = new URL(normalized, opts.targetBaseUrl)\n // Preserve query string.\n const queryIdx = rawUrl.indexOf(\"?\")\n if (queryIdx >= 0) {\n targetUrl.search = rawUrl.slice(queryIdx)\n }\n\n log(`[telegram-proxy] ${req.method} ${rawUrl} -> ${targetUrl.toString()}`)\n\n // Build headers: forward everything except Host, which we rewrite.\n const headers: Record<string, string | string[]> = {}\n for (const [key, value] of Object.entries(req.headers)) {\n if (value !== undefined) {\n headers[key] = value\n }\n }\n headers.host = targetUrl.host\n\n const proxyReq = request(\n {\n hostname: targetUrl.hostname,\n port: targetUrl.port || (targetUrl.protocol === \"https:\" ? 443 : 80),\n path: targetUrl.pathname + targetUrl.search,\n method: req.method,\n headers,\n },\n proxyRes => {\n res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers)\n proxyRes.pipe(res)\n },\n )\n\n proxyReq.on(\"error\", err => {\n log(`[telegram-proxy] upstream error: ${err.message}`)\n if (!res.headersSent) {\n res.writeHead(502, { \"content-type\": \"application/json\" })\n res.end(JSON.stringify({ error: \"bad_gateway\" }))\n }\n })\n\n // The proxy does not follow redirects; it passes the upstream response\n // (including 301/302) straight through to the caller. This is the default\n // behaviour of node:http.request.\n\n req.pipe(proxyReq)\n}\n\n/**\n * Decode percent-encoding and collapse `.` / `..` segments.\n * Returns the raw path part on malformed percent-encoding so it fails the\n * `/inbound/` prefix check downstream.\n */\nfunction normalizePath(urlPath: string): string {\n const pathPart = urlPath.split(\"?\")[0] ?? urlPath\n\n let decoded: string\n try {\n decoded = decodeURIComponent(pathPart)\n } catch {\n // Malformed percent-encoding — return raw so it fails the allowlist.\n return pathPart\n }\n\n const hasTrailingSlash = decoded.endsWith(\"/\") && decoded !== \"/\"\n\n const segments = decoded.split(\"/\").filter(s => s !== \"\" && s !== \".\")\n const normalized: string[] = []\n for (const seg of segments) {\n if (seg === \"..\") {\n normalized.pop()\n } else {\n normalized.push(seg)\n }\n }\n\n let result = \"/\" + normalized.join(\"/\")\n if (hasTrailingSlash && !result.endsWith(\"/\")) {\n result += \"/\"\n }\n return result\n}\n"]}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { S as SessionConfig, R as RouteSpec, P as Posture, E as EffortLevel, C as ContextProfile } from './session-config-DIf6wYYP.js';
|
|
3
|
+
import '@agentproto/auth';
|
|
4
|
+
import './context-continuity-B9n0t0v-.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* User-owned spawn presets.
|
|
8
|
+
*
|
|
9
|
+
* This is deliberately separate from `preset-tools.ts`: provider presets are
|
|
10
|
+
* static gateway definitions shipped by packages, while a UserPreset is a
|
|
11
|
+
* private saved combination of the orthogonal session-config axes.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** A reusable, user-scoped subset of the spawn/session config axes. */
|
|
15
|
+
interface UserPreset extends Partial<SessionConfig> {
|
|
16
|
+
/** Stable machine-local id, e.g. `fast-deepseek`. */
|
|
17
|
+
id: string;
|
|
18
|
+
/** Human-readable name shown by CLI and editor pickers. */
|
|
19
|
+
label: string;
|
|
20
|
+
/** Adapter harness to use. Omitted means the caller selects one. */
|
|
21
|
+
adapter?: string;
|
|
22
|
+
/** Canonical harness slug — alias for `adapter`. */
|
|
23
|
+
harness?: string;
|
|
24
|
+
model?: string;
|
|
25
|
+
route?: RouteSpec;
|
|
26
|
+
access?: {
|
|
27
|
+
profileRef?: string;
|
|
28
|
+
};
|
|
29
|
+
posture?: Posture;
|
|
30
|
+
effort?: EffortLevel;
|
|
31
|
+
contextProfile?: ContextProfile;
|
|
32
|
+
/** Working directory the favorite pins to. When set, a spawn from this
|
|
33
|
+
* preset lands here regardless of the caller's active folder — the axis
|
|
34
|
+
* that makes a favorite fully location-pinned (true zero-input). Omitted
|
|
35
|
+
* means the caller's cwd ladder resolves it as before. */
|
|
36
|
+
cwd?: string;
|
|
37
|
+
/** Skills to preload for a spawn from this preset — the same axis as
|
|
38
|
+
* `SpawnAgentSessionInput.skills`. Omitted means the adapter/defaults
|
|
39
|
+
* decide. */
|
|
40
|
+
skills?: string[];
|
|
41
|
+
}
|
|
42
|
+
declare const userPresetsFileSchema: z.ZodObject<{
|
|
43
|
+
version: z.ZodLiteral<1>;
|
|
44
|
+
presets: z.ZodArray<z.ZodObject<{
|
|
45
|
+
id: z.ZodString;
|
|
46
|
+
label: z.ZodString;
|
|
47
|
+
adapter: z.ZodOptional<z.ZodString>;
|
|
48
|
+
harness: z.ZodOptional<z.ZodString>;
|
|
49
|
+
model: z.ZodOptional<z.ZodString>;
|
|
50
|
+
route: z.ZodOptional<z.ZodObject<{
|
|
51
|
+
gateway: z.ZodString;
|
|
52
|
+
baseUrl: z.ZodOptional<z.ZodString>;
|
|
53
|
+
}, z.core.$strip>>;
|
|
54
|
+
access: z.ZodOptional<z.ZodObject<{
|
|
55
|
+
profileRef: z.ZodOptional<z.ZodString>;
|
|
56
|
+
}, z.core.$strip>>;
|
|
57
|
+
posture: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
|
|
58
|
+
default: "default";
|
|
59
|
+
plan: "plan";
|
|
60
|
+
"accept-edits": "accept-edits";
|
|
61
|
+
bypass: "bypass";
|
|
62
|
+
"read-only": "read-only";
|
|
63
|
+
}>, z.ZodObject<{
|
|
64
|
+
harnessModeId: z.ZodString;
|
|
65
|
+
}, z.core.$strip>]>>;
|
|
66
|
+
effort: z.ZodOptional<z.ZodEnum<{
|
|
67
|
+
low: "low";
|
|
68
|
+
medium: "medium";
|
|
69
|
+
high: "high";
|
|
70
|
+
xhigh: "xhigh";
|
|
71
|
+
max: "max";
|
|
72
|
+
ultracode: "ultracode";
|
|
73
|
+
}>>;
|
|
74
|
+
contextProfile: z.ZodOptional<z.ZodString>;
|
|
75
|
+
cwd: z.ZodOptional<z.ZodString>;
|
|
76
|
+
skills: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
77
|
+
}, z.core.$strip>>;
|
|
78
|
+
}, z.core.$strip>;
|
|
79
|
+
type UserPresetsFile = z.infer<typeof userPresetsFileSchema>;
|
|
80
|
+
declare function userPresetsPath(): string;
|
|
81
|
+
/** Missing or malformed user config is treated as empty — a bad preset must
|
|
82
|
+
* never prevent the daemon from starting. Writes always restore valid JSON. */
|
|
83
|
+
declare function loadUserPresets(): Promise<UserPresetsFile>;
|
|
84
|
+
declare function listUserPresets(): Promise<UserPreset[]>;
|
|
85
|
+
declare function getUserPreset(id: string): Promise<UserPreset | undefined>;
|
|
86
|
+
/** Add or replace a preset by id. The parser makes this the single validation
|
|
87
|
+
* boundary for CLI, MCP and editor callers. */
|
|
88
|
+
declare function saveUserPreset(preset: UserPreset): Promise<void>;
|
|
89
|
+
declare function deleteUserPreset(id: string): Promise<boolean>;
|
|
90
|
+
|
|
91
|
+
export { type UserPreset, type UserPresetsFile, deleteUserPreset, getUserPreset, listUserPresets, loadUserPresets, saveUserPreset, userPresetsPath };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { readFile, mkdir, writeFile } from 'fs/promises';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { resolve, join } from 'path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @agentproto/runtime v0.1.0-alpha
|
|
8
|
+
* Long-running gateway: MCP server + HTTP transport + HEARTBEAT autonomy + conversation persistence over a workspace dir.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
var effortSchema = z.enum(["low", "medium", "high", "xhigh", "max", "ultracode"]);
|
|
12
|
+
var postureSchema = z.union([
|
|
13
|
+
z.enum(["default", "plan", "accept-edits", "bypass", "read-only"]),
|
|
14
|
+
z.object({ harnessModeId: z.string().min(1) })
|
|
15
|
+
]);
|
|
16
|
+
var routeSchema = z.object({
|
|
17
|
+
gateway: z.string().min(1),
|
|
18
|
+
baseUrl: z.string().url().optional()
|
|
19
|
+
});
|
|
20
|
+
var userPresetSchema = z.object({
|
|
21
|
+
id: z.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
22
|
+
label: z.string().min(1),
|
|
23
|
+
adapter: z.string().min(1).optional(),
|
|
24
|
+
harness: z.string().min(1).optional(),
|
|
25
|
+
model: z.string().min(1).optional(),
|
|
26
|
+
route: routeSchema.optional(),
|
|
27
|
+
access: z.object({ profileRef: z.string().min(1).optional() }).optional(),
|
|
28
|
+
posture: postureSchema.optional(),
|
|
29
|
+
effort: effortSchema.optional(),
|
|
30
|
+
contextProfile: z.string().min(1).optional(),
|
|
31
|
+
cwd: z.string().min(1).optional(),
|
|
32
|
+
skills: z.array(z.string().min(1)).optional()
|
|
33
|
+
});
|
|
34
|
+
var userPresetsFileSchema = z.object({
|
|
35
|
+
version: z.literal(1),
|
|
36
|
+
presets: z.array(userPresetSchema)
|
|
37
|
+
});
|
|
38
|
+
function emptyFile() {
|
|
39
|
+
return { version: 1, presets: [] };
|
|
40
|
+
}
|
|
41
|
+
function userPresetsPath() {
|
|
42
|
+
return resolve(homedir(), ".agentproto", "presets.json");
|
|
43
|
+
}
|
|
44
|
+
async function loadUserPresets() {
|
|
45
|
+
try {
|
|
46
|
+
return userPresetsFileSchema.parse(JSON.parse(await readFile(userPresetsPath(), "utf8")));
|
|
47
|
+
} catch {
|
|
48
|
+
return emptyFile();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function writeUserPresets(file) {
|
|
52
|
+
const dir = join(homedir(), ".agentproto");
|
|
53
|
+
await mkdir(dir, { recursive: true });
|
|
54
|
+
await writeFile(userPresetsPath(), JSON.stringify(file, null, 2) + "\n", {
|
|
55
|
+
encoding: "utf8",
|
|
56
|
+
mode: 384
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
async function listUserPresets() {
|
|
60
|
+
return (await loadUserPresets()).presets;
|
|
61
|
+
}
|
|
62
|
+
async function getUserPreset(id) {
|
|
63
|
+
return (await loadUserPresets()).presets.find((preset) => preset.id === id);
|
|
64
|
+
}
|
|
65
|
+
async function saveUserPreset(preset) {
|
|
66
|
+
const validated = userPresetSchema.parse(preset);
|
|
67
|
+
const file = await loadUserPresets();
|
|
68
|
+
const index = file.presets.findIndex((existing) => existing.id === validated.id);
|
|
69
|
+
if (index === -1) file.presets.push(validated);
|
|
70
|
+
else file.presets[index] = validated;
|
|
71
|
+
await writeUserPresets(file);
|
|
72
|
+
}
|
|
73
|
+
async function deleteUserPreset(id) {
|
|
74
|
+
const file = await loadUserPresets();
|
|
75
|
+
const index = file.presets.findIndex((preset) => preset.id === id);
|
|
76
|
+
if (index === -1) return false;
|
|
77
|
+
file.presets.splice(index, 1);
|
|
78
|
+
await writeUserPresets(file);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export { deleteUserPreset, getUserPreset, listUserPresets, loadUserPresets, saveUserPreset, userPresetsPath };
|
|
83
|
+
//# sourceMappingURL=user-presets.mjs.map
|
|
84
|
+
//# sourceMappingURL=user-presets.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/user-presets.ts"],"names":[],"mappings":"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","file":"user-presets.mjs","sourcesContent":["/**\n * User-owned spawn presets.\n *\n * This is deliberately separate from `preset-tools.ts`: provider presets are\n * static gateway definitions shipped by packages, while a UserPreset is a\n * private saved combination of the orthogonal session-config axes.\n */\n\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\"\nimport { homedir } from \"node:os\"\nimport { join, resolve } from \"node:path\"\nimport { z } from \"zod\"\nimport type {\n ContextProfile,\n EffortLevel,\n Posture,\n RouteSpec,\n SessionConfig,\n} from \"./session-config.js\"\n\nconst effortSchema = z.enum([\"low\", \"medium\", \"high\", \"xhigh\", \"max\", \"ultracode\"])\nconst postureSchema = z.union([\n z.enum([\"default\", \"plan\", \"accept-edits\", \"bypass\", \"read-only\"]),\n z.object({ harnessModeId: z.string().min(1) }),\n])\nconst routeSchema = z.object({\n gateway: z.string().min(1),\n baseUrl: z.string().url().optional(),\n})\n\n/** A reusable, user-scoped subset of the spawn/session config axes. */\nexport interface UserPreset extends Partial<SessionConfig> {\n /** Stable machine-local id, e.g. `fast-deepseek`. */\n id: string\n /** Human-readable name shown by CLI and editor pickers. */\n label: string\n /** Adapter harness to use. Omitted means the caller selects one. */\n adapter?: string\n /** Canonical harness slug — alias for `adapter`. */\n harness?: string\n model?: string\n route?: RouteSpec\n access?: { profileRef?: string }\n posture?: Posture\n effort?: EffortLevel\n contextProfile?: ContextProfile\n /** Working directory the favorite pins to. When set, a spawn from this\n * preset lands here regardless of the caller's active folder — the axis\n * that makes a favorite fully location-pinned (true zero-input). Omitted\n * means the caller's cwd ladder resolves it as before. */\n cwd?: string\n /** Skills to preload for a spawn from this preset — the same axis as\n * `SpawnAgentSessionInput.skills`. Omitted means the adapter/defaults\n * decide. */\n skills?: string[]\n}\n\nconst userPresetSchema = z.object({\n id: z.string().regex(/^[a-z0-9][a-z0-9-]*$/),\n label: z.string().min(1),\n adapter: z.string().min(1).optional(),\n harness: z.string().min(1).optional(),\n model: z.string().min(1).optional(),\n route: routeSchema.optional(),\n access: z.object({ profileRef: z.string().min(1).optional() }).optional(),\n posture: postureSchema.optional(),\n effort: effortSchema.optional(),\n contextProfile: z.string().min(1).optional(),\n cwd: z.string().min(1).optional(),\n skills: z.array(z.string().min(1)).optional(),\n}) satisfies z.ZodType<UserPreset>\n\nconst userPresetsFileSchema = z.object({\n version: z.literal(1),\n presets: z.array(userPresetSchema),\n})\n\nexport type UserPresetsFile = z.infer<typeof userPresetsFileSchema>\n\nfunction emptyFile(): UserPresetsFile {\n return { version: 1, presets: [] }\n}\n\nexport function userPresetsPath(): string {\n return resolve(homedir(), \".agentproto\", \"presets.json\")\n}\n\n/** Missing or malformed user config is treated as empty — a bad preset must\n * never prevent the daemon from starting. Writes always restore valid JSON. */\nexport async function loadUserPresets(): Promise<UserPresetsFile> {\n try {\n return userPresetsFileSchema.parse(JSON.parse(await readFile(userPresetsPath(), \"utf8\")))\n } catch {\n return emptyFile()\n }\n}\n\nasync function writeUserPresets(file: UserPresetsFile): Promise<void> {\n const dir = join(homedir(), \".agentproto\")\n await mkdir(dir, { recursive: true })\n await writeFile(userPresetsPath(), JSON.stringify(file, null, 2) + \"\\n\", {\n encoding: \"utf8\",\n mode: 0o600,\n })\n}\n\nexport async function listUserPresets(): Promise<UserPreset[]> {\n return (await loadUserPresets()).presets\n}\n\nexport async function getUserPreset(id: string): Promise<UserPreset | undefined> {\n return (await loadUserPresets()).presets.find(preset => preset.id === id)\n}\n\n/** Add or replace a preset by id. The parser makes this the single validation\n * boundary for CLI, MCP and editor callers. */\nexport async function saveUserPreset(preset: UserPreset): Promise<void> {\n const validated = userPresetSchema.parse(preset)\n const file = await loadUserPresets()\n const index = file.presets.findIndex(existing => existing.id === validated.id)\n if (index === -1) file.presets.push(validated)\n else file.presets[index] = validated\n await writeUserPresets(file)\n}\n\nexport async function deleteUserPreset(id: string): Promise<boolean> {\n const file = await loadUserPresets()\n const index = file.presets.findIndex(preset => preset.id === id)\n if (index === -1) return false\n file.presets.splice(index, 1)\n await writeUserPresets(file)\n return true\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { promises, readFileSync } from 'fs';
|
|
1
|
+
import { promises, readFileSync, realpathSync } from 'fs';
|
|
2
2
|
import { homedir } from 'os';
|
|
3
3
|
import { resolve, dirname, isAbsolute } from 'path';
|
|
4
4
|
|
|
@@ -113,9 +113,19 @@ function setActiveWorkspace(config, slug) {
|
|
|
113
113
|
function findWorkspace(config, slug) {
|
|
114
114
|
return config.workspaces.find((w) => w.slug === sanitizeSlug(slug));
|
|
115
115
|
}
|
|
116
|
+
function canonical(p) {
|
|
117
|
+
try {
|
|
118
|
+
return realpathSync(resolve(p));
|
|
119
|
+
} catch {
|
|
120
|
+
return resolve(p);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
116
123
|
function findWorkspaceByPath(config, dir) {
|
|
117
|
-
const resolved =
|
|
118
|
-
const candidates = config.workspaces.filter((w) =>
|
|
124
|
+
const resolved = canonical(dir);
|
|
125
|
+
const candidates = config.workspaces.filter((w) => {
|
|
126
|
+
const wPath = canonical(w.path);
|
|
127
|
+
return resolved.startsWith(wPath + "/") || resolved === wPath;
|
|
128
|
+
}).sort((a, b) => b.path.length - a.path.length);
|
|
119
129
|
return candidates[0];
|
|
120
130
|
}
|
|
121
131
|
function getActiveWorkspace(config) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/workspaces-config.ts"],"names":["fs"],"mappings":"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","file":"workspaces-config.mjs","sourcesContent":["/**\n * `~/.agentproto/workspaces.json` — single source of truth for the\n * local agentproto control plane. Tracks which directories the user\n * has opted into as agentproto workspaces, plus which one is \"active\"\n * (the daemon's default working set when no workspace is named in\n * a tool call).\n *\n * Why a config file instead of recreating an MCP entry per workspace:\n * - One daemon = one MCP entry in the user's IDE (clean to read in\n * `claude mcp list`, no `dapper_willow` / `noble_lantern` spam).\n * - The daemon walks `workspaces[]` at boot and exposes them all;\n * tools that need a target take an optional `workspace` arg and\n * fall back to `active` when omitted.\n * - The CLI (`agentproto workspace add/list/remove/use`), the daemon,\n * and the guilde-web onboarding dialog all read+write the same\n * file, so changes from one surface are immediately visible to\n * the others.\n *\n * File layout is intentionally boring (small, hand-editable JSON) —\n * future fields go on top, never break v1 readers.\n */\n\nimport { promises as fs, readFileSync } from \"node:fs\"\nimport { homedir } from \"node:os\"\nimport { dirname, isAbsolute, resolve } from \"node:path\"\n\nexport const WORKSPACES_CONFIG_VERSION = 1 as const\n\nexport interface WorkspaceEntry {\n /** Stable handle — what tools accept as `workspace`. Lowercase\n * ASCII, hyphen-only. The CLI sanitises arbitrary names down to\n * this shape so paste-from-finder Just Works. */\n slug: string\n /** Absolute filesystem path. Required absolute so the daemon can\n * serve the workspace regardless of its own cwd. */\n path: string\n /** ISO-8601 — first time the entry was added. Pure metadata, no\n * behavioural impact. */\n addedAt: string\n /** Last time the entry was touched (renamed, path edited, used as\n * active). Lets the CLI sort by recency without an N+1 stat. */\n updatedAt: string\n /** Free-text label the user can attach so workspaces with similar\n * slugs stay distinguishable in `agentproto workspace list`. */\n label?: string\n}\n\nexport interface WorkspacesConfig {\n version: typeof WORKSPACES_CONFIG_VERSION\n /** Active workspace slug — daemon defaults to this when no\n * `workspace` arg is passed. May be undefined when no workspaces\n * are registered yet. */\n active?: string\n workspaces: WorkspaceEntry[]\n}\n\nexport const DEFAULT_CONFIG_DIR = (): string =>\n resolve(homedir(), \".agentproto\")\nexport const DEFAULT_CONFIG_PATH = (): string =>\n resolve(DEFAULT_CONFIG_DIR(), \"workspaces.json\")\n\n/** Reduce arbitrary user input to a safe slug. Aligns with what most\n * tooling validates against (`/^[a-z0-9][a-z0-9-]{0,63}$/`) so the\n * daemon never has to encode it for URLs or filesystem paths. */\nexport function sanitizeSlug(input: string): string {\n const trimmed = input.trim().toLowerCase()\n const cleaned = trimmed\n .replace(/[^a-z0-9_-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 64)\n // Edge case: input was entirely non-conforming — fall back to a\n // generic so we never return an empty string.\n return cleaned || \"workspace\"\n}\n\n/** Read the config from disk. Returns an empty config (no workspaces,\n * no active) when the file is missing — this is the legitimate\n * first-boot state, not an error. */\nexport async function loadWorkspacesConfig(\n path: string = DEFAULT_CONFIG_PATH()\n): Promise<WorkspacesConfig> {\n let raw: string\n try {\n raw = await fs.readFile(path, \"utf8\")\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return { version: WORKSPACES_CONFIG_VERSION, workspaces: [] }\n }\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 the file or fix it manually.`\n )\n }\n return normalizeConfig(parsed)\n}\n\n/** Sync twin of `loadWorkspacesConfig`, sharing its normalisation (and\n * therefore its slug sanitisation) exactly.\n *\n * Exists because bucket resolution runs on paths that cannot await: the\n * sessions registry loads its history synchronously at construction so\n * the dashboard has rows immediately, and flushes synchronously on\n * `process.on(\"exit\")` where Node won't await. Both need to know which\n * slugs are registered. The file is <1KB, so the sync read is cheaper\n * than the machinery to avoid it. */\nexport function loadWorkspacesConfigSync(\n path: string = DEFAULT_CONFIG_PATH()\n): WorkspacesConfig {\n let raw: string\n try {\n raw = readFileSync(path, \"utf8\")\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return { version: WORKSPACES_CONFIG_VERSION, workspaces: [] }\n }\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 the file or fix it manually.`\n )\n }\n return normalizeConfig(parsed)\n}\n\n/** Write the config back. Creates the parent directory if missing.\n * Atomic-ish: writes to a temp file first, then renames over the\n * target — avoids the partial-write window where a concurrent reader\n * sees half a JSON object. */\nexport async function saveWorkspacesConfig(\n config: WorkspacesConfig,\n path: string = DEFAULT_CONFIG_PATH()\n): Promise<void> {\n const normalized = normalizeConfig(config)\n await fs.mkdir(dirname(path), { recursive: true })\n const tmp = `${path}.tmp.${process.pid}`\n await fs.writeFile(tmp, JSON.stringify(normalized, null, 2) + \"\\n\", \"utf8\")\n await fs.rename(tmp, path)\n}\n\n/**\n * Pure helpers — caller manages persistence. Returns a NEW config\n * (never mutates) so callers can compose multiple updates before\n * a single save.\n */\nexport function addWorkspace(\n config: WorkspacesConfig,\n input: { slug: string; path: string; label?: string }\n): WorkspacesConfig {\n if (!isAbsolute(input.path)) {\n throw new Error(\n `addWorkspace: path must be absolute, got \"${input.path}\".`\n )\n }\n const slug = sanitizeSlug(input.slug)\n const now = new Date().toISOString()\n const existingIdx = config.workspaces.findIndex(w => w.slug === slug)\n const next: WorkspaceEntry = existingIdx >= 0\n ? {\n ...config.workspaces[existingIdx]!,\n path: input.path,\n ...(input.label !== undefined ? { label: input.label } : {}),\n updatedAt: now,\n }\n : {\n slug,\n path: input.path,\n ...(input.label ? { label: input.label } : {}),\n addedAt: now,\n updatedAt: now,\n }\n const workspaces = [...config.workspaces]\n if (existingIdx >= 0) workspaces[existingIdx] = next\n else workspaces.push(next)\n // First-add becomes active automatically — saves the user a\n // separate `workspace use` call right after a fresh `add`.\n const active = config.active ?? slug\n return { ...config, workspaces, active }\n}\n\nexport function removeWorkspace(\n config: WorkspacesConfig,\n slug: string\n): WorkspacesConfig {\n const sanitised = sanitizeSlug(slug)\n const workspaces = config.workspaces.filter(w => w.slug !== sanitised)\n // If the removed workspace was active, hand off to whatever's\n // first — predictable and avoids leaving the daemon in an\n // unreachable state when the user removes the only one.\n let active = config.active\n if (active === sanitised) {\n active = workspaces[0]?.slug\n }\n const next: WorkspacesConfig = { ...config, workspaces }\n if (active !== undefined) next.active = active\n else delete next.active\n return next\n}\n\nexport function setActiveWorkspace(\n config: WorkspacesConfig,\n slug: string\n): WorkspacesConfig {\n const sanitised = sanitizeSlug(slug)\n if (!config.workspaces.some(w => w.slug === sanitised)) {\n throw new Error(\n `setActiveWorkspace: no workspace registered with slug \"${sanitised}\". ` +\n `Run \\`agentproto workspace add <path> --slug ${sanitised}\\` first.`\n )\n }\n return { ...config, active: sanitised }\n}\n\nexport function findWorkspace(\n config: WorkspacesConfig,\n slug: string\n): WorkspaceEntry | undefined {\n return config.workspaces.find(w => w.slug === sanitizeSlug(slug))\n}\n\n/** Find a workspace whose path is an ancestor of (or equal to) the\n * given directory. Returns the most specific match (longest path)\n * when multiple workspaces nest under the same root. */\nexport function findWorkspaceByPath(\n config: WorkspacesConfig,\n dir: string\n): WorkspaceEntry | undefined {\n const resolved = resolve(dir)\n const candidates = config.workspaces\n .filter(w => resolved.startsWith(resolve(w.path) + \"/\") || resolved === resolve(w.path))\n .sort((a, b) => b.path.length - a.path.length)\n return candidates[0]\n}\n\n/** The active workspace, or undefined when none is registered. Pure\n * convenience to avoid re-implementing the lookup at every caller. */\nexport function getActiveWorkspace(\n config: WorkspacesConfig\n): WorkspaceEntry | undefined {\n if (!config.active) return config.workspaces[0]\n return findWorkspace(config, config.active) ?? config.workspaces[0]\n}\n\n/**\n * Coerce arbitrary parsed JSON into a valid config. Skips entries\n * with missing required fields rather than throwing — partial recovery\n * is friendlier than \"your config file is corrupted, here's a stack\n * trace\". Logs nothing here; the CLI shells that consume this should\n * decide whether to surface a warning.\n */\nfunction normalizeConfig(parsed: unknown): WorkspacesConfig {\n if (!parsed || typeof parsed !== \"object\") {\n return { version: WORKSPACES_CONFIG_VERSION, workspaces: [] }\n }\n const obj = parsed as Record<string, unknown>\n const workspaces: WorkspaceEntry[] = []\n if (Array.isArray(obj.workspaces)) {\n for (const entry of obj.workspaces) {\n if (!entry || typeof entry !== \"object\") continue\n const e = entry as Record<string, unknown>\n const slug = typeof e.slug === \"string\" ? sanitizeSlug(e.slug) : \"\"\n const path = typeof e.path === \"string\" ? e.path : \"\"\n if (!slug || !path) continue\n const addedAt =\n typeof e.addedAt === \"string\" ? e.addedAt : new Date().toISOString()\n const updatedAt = typeof e.updatedAt === \"string\" ? e.updatedAt : addedAt\n const we: WorkspaceEntry = { slug, path, addedAt, updatedAt }\n if (typeof e.label === \"string\" && e.label.trim()) {\n we.label = e.label.trim()\n }\n workspaces.push(we)\n }\n }\n // De-dupe by slug (last wins) — the file is hand-editable so a\n // typo could have produced two entries with the same slug.\n const dedup = new Map<string, WorkspaceEntry>()\n for (const w of workspaces) dedup.set(w.slug, w)\n const finalList = Array.from(dedup.values())\n const active =\n typeof obj.active === \"string\" && finalList.some(w => w.slug === obj.active)\n ? (obj.active as string)\n : finalList[0]?.slug\n const out: WorkspacesConfig = {\n version: WORKSPACES_CONFIG_VERSION,\n workspaces: finalList,\n }\n if (active !== undefined) out.active = active\n return out\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/workspaces-config.ts"],"names":["fs"],"mappings":"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","file":"workspaces-config.mjs","sourcesContent":["/**\n * `~/.agentproto/workspaces.json` — single source of truth for the\n * local agentproto control plane. Tracks which directories the user\n * has opted into as agentproto workspaces, plus which one is \"active\"\n * (the daemon's default working set when no workspace is named in\n * a tool call).\n *\n * Why a config file instead of recreating an MCP entry per workspace:\n * - One daemon = one MCP entry in the user's IDE (clean to read in\n * `claude mcp list`, no `dapper_willow` / `noble_lantern` spam).\n * - The daemon walks `workspaces[]` at boot and exposes them all;\n * tools that need a target take an optional `workspace` arg and\n * fall back to `active` when omitted.\n * - The CLI (`agentproto workspace add/list/remove/use`), the daemon,\n * and the guilde-web onboarding dialog all read+write the same\n * file, so changes from one surface are immediately visible to\n * the others.\n *\n * File layout is intentionally boring (small, hand-editable JSON) —\n * future fields go on top, never break v1 readers.\n */\n\nimport { promises as fs, readFileSync, realpathSync } from \"node:fs\"\nimport { homedir } from \"node:os\"\nimport { dirname, isAbsolute, resolve } from \"node:path\"\n\nexport const WORKSPACES_CONFIG_VERSION = 1 as const\n\nexport interface WorkspaceEntry {\n /** Stable handle — what tools accept as `workspace`. Lowercase\n * ASCII, hyphen-only. The CLI sanitises arbitrary names down to\n * this shape so paste-from-finder Just Works. */\n slug: string\n /** Absolute filesystem path. Required absolute so the daemon can\n * serve the workspace regardless of its own cwd. */\n path: string\n /** ISO-8601 — first time the entry was added. Pure metadata, no\n * behavioural impact. */\n addedAt: string\n /** Last time the entry was touched (renamed, path edited, used as\n * active). Lets the CLI sort by recency without an N+1 stat. */\n updatedAt: string\n /** Free-text label the user can attach so workspaces with similar\n * slugs stay distinguishable in `agentproto workspace list`. */\n label?: string\n}\n\nexport interface WorkspacesConfig {\n version: typeof WORKSPACES_CONFIG_VERSION\n /** Active workspace slug — daemon defaults to this when no\n * `workspace` arg is passed. May be undefined when no workspaces\n * are registered yet. */\n active?: string\n workspaces: WorkspaceEntry[]\n}\n\nexport const DEFAULT_CONFIG_DIR = (): string =>\n resolve(homedir(), \".agentproto\")\nexport const DEFAULT_CONFIG_PATH = (): string =>\n resolve(DEFAULT_CONFIG_DIR(), \"workspaces.json\")\n\n/** Reduce arbitrary user input to a safe slug. Aligns with what most\n * tooling validates against (`/^[a-z0-9][a-z0-9-]{0,63}$/`) so the\n * daemon never has to encode it for URLs or filesystem paths. */\nexport function sanitizeSlug(input: string): string {\n const trimmed = input.trim().toLowerCase()\n const cleaned = trimmed\n .replace(/[^a-z0-9_-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 64)\n // Edge case: input was entirely non-conforming — fall back to a\n // generic so we never return an empty string.\n return cleaned || \"workspace\"\n}\n\n/** Read the config from disk. Returns an empty config (no workspaces,\n * no active) when the file is missing — this is the legitimate\n * first-boot state, not an error. */\nexport async function loadWorkspacesConfig(\n path: string = DEFAULT_CONFIG_PATH()\n): Promise<WorkspacesConfig> {\n let raw: string\n try {\n raw = await fs.readFile(path, \"utf8\")\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return { version: WORKSPACES_CONFIG_VERSION, workspaces: [] }\n }\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 the file or fix it manually.`\n )\n }\n return normalizeConfig(parsed)\n}\n\n/** Sync twin of `loadWorkspacesConfig`, sharing its normalisation (and\n * therefore its slug sanitisation) exactly.\n *\n * Exists because bucket resolution runs on paths that cannot await: the\n * sessions registry loads its history synchronously at construction so\n * the dashboard has rows immediately, and flushes synchronously on\n * `process.on(\"exit\")` where Node won't await. Both need to know which\n * slugs are registered. The file is <1KB, so the sync read is cheaper\n * than the machinery to avoid it. */\nexport function loadWorkspacesConfigSync(\n path: string = DEFAULT_CONFIG_PATH()\n): WorkspacesConfig {\n let raw: string\n try {\n raw = readFileSync(path, \"utf8\")\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return { version: WORKSPACES_CONFIG_VERSION, workspaces: [] }\n }\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 the file or fix it manually.`\n )\n }\n return normalizeConfig(parsed)\n}\n\n/** Write the config back. Creates the parent directory if missing.\n * Atomic-ish: writes to a temp file first, then renames over the\n * target — avoids the partial-write window where a concurrent reader\n * sees half a JSON object. */\nexport async function saveWorkspacesConfig(\n config: WorkspacesConfig,\n path: string = DEFAULT_CONFIG_PATH()\n): Promise<void> {\n const normalized = normalizeConfig(config)\n await fs.mkdir(dirname(path), { recursive: true })\n const tmp = `${path}.tmp.${process.pid}`\n await fs.writeFile(tmp, JSON.stringify(normalized, null, 2) + \"\\n\", \"utf8\")\n await fs.rename(tmp, path)\n}\n\n/**\n * Pure helpers — caller manages persistence. Returns a NEW config\n * (never mutates) so callers can compose multiple updates before\n * a single save.\n */\nexport function addWorkspace(\n config: WorkspacesConfig,\n input: { slug: string; path: string; label?: string }\n): WorkspacesConfig {\n if (!isAbsolute(input.path)) {\n throw new Error(\n `addWorkspace: path must be absolute, got \"${input.path}\".`\n )\n }\n const slug = sanitizeSlug(input.slug)\n const now = new Date().toISOString()\n const existingIdx = config.workspaces.findIndex(w => w.slug === slug)\n const next: WorkspaceEntry = existingIdx >= 0\n ? {\n ...config.workspaces[existingIdx]!,\n path: input.path,\n ...(input.label !== undefined ? { label: input.label } : {}),\n updatedAt: now,\n }\n : {\n slug,\n path: input.path,\n ...(input.label ? { label: input.label } : {}),\n addedAt: now,\n updatedAt: now,\n }\n const workspaces = [...config.workspaces]\n if (existingIdx >= 0) workspaces[existingIdx] = next\n else workspaces.push(next)\n // First-add becomes active automatically — saves the user a\n // separate `workspace use` call right after a fresh `add`.\n const active = config.active ?? slug\n return { ...config, workspaces, active }\n}\n\nexport function removeWorkspace(\n config: WorkspacesConfig,\n slug: string\n): WorkspacesConfig {\n const sanitised = sanitizeSlug(slug)\n const workspaces = config.workspaces.filter(w => w.slug !== sanitised)\n // If the removed workspace was active, hand off to whatever's\n // first — predictable and avoids leaving the daemon in an\n // unreachable state when the user removes the only one.\n let active = config.active\n if (active === sanitised) {\n active = workspaces[0]?.slug\n }\n const next: WorkspacesConfig = { ...config, workspaces }\n if (active !== undefined) next.active = active\n else delete next.active\n return next\n}\n\nexport function setActiveWorkspace(\n config: WorkspacesConfig,\n slug: string\n): WorkspacesConfig {\n const sanitised = sanitizeSlug(slug)\n if (!config.workspaces.some(w => w.slug === sanitised)) {\n throw new Error(\n `setActiveWorkspace: no workspace registered with slug \"${sanitised}\". ` +\n `Run \\`agentproto workspace add <path> --slug ${sanitised}\\` first.`\n )\n }\n return { ...config, active: sanitised }\n}\n\nexport function findWorkspace(\n config: WorkspacesConfig,\n slug: string\n): WorkspaceEntry | undefined {\n return config.workspaces.find(w => w.slug === sanitizeSlug(slug))\n}\n\n/** Resolve a path through `realpathSync` so symlinked roots (macOS `/tmp` →\n * `/private/tmp`, a symlinked volume, …) compare equal to their real\n * location. Falls back to a lexical `resolve` when the path doesn't exist\n * on disk — `realpathSync` throws on ENOENT, and a not-yet-existing path\n * still needs a comparable value. */\nfunction canonical(p: string): string {\n try {\n return realpathSync(resolve(p))\n } catch {\n return resolve(p)\n }\n}\n\n/** Find a workspace whose path is an ancestor of (or equal to) the\n * given directory. Returns the most specific match (longest path)\n * when multiple workspaces nest under the same root. */\nexport function findWorkspaceByPath(\n config: WorkspacesConfig,\n dir: string\n): WorkspaceEntry | undefined {\n const resolved = canonical(dir)\n const candidates = config.workspaces\n .filter(w => {\n const wPath = canonical(w.path)\n return resolved.startsWith(wPath + \"/\") || resolved === wPath\n })\n .sort((a, b) => b.path.length - a.path.length)\n return candidates[0]\n}\n\n/** The active workspace, or undefined when none is registered. Pure\n * convenience to avoid re-implementing the lookup at every caller. */\nexport function getActiveWorkspace(\n config: WorkspacesConfig\n): WorkspaceEntry | undefined {\n if (!config.active) return config.workspaces[0]\n return findWorkspace(config, config.active) ?? config.workspaces[0]\n}\n\n/**\n * Coerce arbitrary parsed JSON into a valid config. Skips entries\n * with missing required fields rather than throwing — partial recovery\n * is friendlier than \"your config file is corrupted, here's a stack\n * trace\". Logs nothing here; the CLI shells that consume this should\n * decide whether to surface a warning.\n */\nfunction normalizeConfig(parsed: unknown): WorkspacesConfig {\n if (!parsed || typeof parsed !== \"object\") {\n return { version: WORKSPACES_CONFIG_VERSION, workspaces: [] }\n }\n const obj = parsed as Record<string, unknown>\n const workspaces: WorkspaceEntry[] = []\n if (Array.isArray(obj.workspaces)) {\n for (const entry of obj.workspaces) {\n if (!entry || typeof entry !== \"object\") continue\n const e = entry as Record<string, unknown>\n const slug = typeof e.slug === \"string\" ? sanitizeSlug(e.slug) : \"\"\n const path = typeof e.path === \"string\" ? e.path : \"\"\n if (!slug || !path) continue\n const addedAt =\n typeof e.addedAt === \"string\" ? e.addedAt : new Date().toISOString()\n const updatedAt = typeof e.updatedAt === \"string\" ? e.updatedAt : addedAt\n const we: WorkspaceEntry = { slug, path, addedAt, updatedAt }\n if (typeof e.label === \"string\" && e.label.trim()) {\n we.label = e.label.trim()\n }\n workspaces.push(we)\n }\n }\n // De-dupe by slug (last wins) — the file is hand-editable so a\n // typo could have produced two entries with the same slug.\n const dedup = new Map<string, WorkspaceEntry>()\n for (const w of workspaces) dedup.set(w.slug, w)\n const finalList = Array.from(dedup.values())\n const active =\n typeof obj.active === \"string\" && finalList.some(w => w.slug === obj.active)\n ? (obj.active as string)\n : finalList[0]?.slug\n const out: WorkspacesConfig = {\n version: WORKSPACES_CONFIG_VERSION,\n workspaces: finalList,\n }\n if (active !== undefined) out.active = active\n return out\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentproto/runtime",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "@agentproto/runtime — long-running gateway that turns an agentproto workspace into a live runtime. Composes @agentproto/mcp-server (CRUD verbs) with HTTP transport, HEARTBEAT.md autonomy loop, and append-only conversation persistence. Drop a workspace dir, point your MCP client at it, and the agent ticks on its own.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agentproto",
|
|
@@ -30,6 +30,11 @@
|
|
|
30
30
|
"import": "./dist/index.mjs",
|
|
31
31
|
"default": "./dist/index.mjs"
|
|
32
32
|
},
|
|
33
|
+
"./catalog-models": {
|
|
34
|
+
"types": "./dist/catalog-models.d.ts",
|
|
35
|
+
"import": "./dist/catalog-models.mjs",
|
|
36
|
+
"default": "./dist/catalog-models.mjs"
|
|
37
|
+
},
|
|
33
38
|
"./conversations": {
|
|
34
39
|
"types": "./dist/conversations.d.ts",
|
|
35
40
|
"import": "./dist/conversations.mjs",
|
|
@@ -46,6 +51,11 @@
|
|
|
46
51
|
"default": "./dist/workspace-fs.mjs"
|
|
47
52
|
},
|
|
48
53
|
"./package.json": "./package.json",
|
|
54
|
+
"./pr-provenance": {
|
|
55
|
+
"types": "./dist/pr-provenance.d.ts",
|
|
56
|
+
"import": "./dist/pr-provenance.mjs",
|
|
57
|
+
"default": "./dist/pr-provenance.mjs"
|
|
58
|
+
},
|
|
49
59
|
"./workspaces-config": {
|
|
50
60
|
"types": "./dist/workspaces-config.d.ts",
|
|
51
61
|
"import": "./dist/workspaces-config.mjs",
|
|
@@ -75,6 +85,16 @@
|
|
|
75
85
|
"types": "./dist/session-story.d.ts",
|
|
76
86
|
"import": "./dist/session-story.mjs",
|
|
77
87
|
"default": "./dist/session-story.mjs"
|
|
88
|
+
},
|
|
89
|
+
"./session-story-panel": {
|
|
90
|
+
"types": "./dist/session-story-panel.d.ts",
|
|
91
|
+
"import": "./dist/session-story-panel.mjs",
|
|
92
|
+
"default": "./dist/session-story-panel.mjs"
|
|
93
|
+
},
|
|
94
|
+
"./user-presets": {
|
|
95
|
+
"types": "./dist/user-presets.d.ts",
|
|
96
|
+
"import": "./dist/user-presets.mjs",
|
|
97
|
+
"default": "./dist/user-presets.mjs"
|
|
78
98
|
}
|
|
79
99
|
},
|
|
80
100
|
"files": [
|
|
@@ -86,42 +106,45 @@
|
|
|
86
106
|
"access": "public"
|
|
87
107
|
},
|
|
88
108
|
"dependencies": {
|
|
89
|
-
"croner": "^9.0.0",
|
|
90
109
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
110
|
+
"croner": "^9.0.0",
|
|
91
111
|
"gray-matter": "^4.0.3",
|
|
92
|
-
"ws": "^8.
|
|
112
|
+
"ws": "^8.21.1",
|
|
93
113
|
"zod": "^4.4.3",
|
|
94
|
-
"@agentproto/acp": "0.6.0",
|
|
95
|
-
"@agentproto/auth": "0.1.1",
|
|
96
|
-
"@agentproto/driver-agent-cli": "2.0.0",
|
|
97
|
-
"@agentproto/workflow": "0.1.0",
|
|
98
|
-
"@agentproto/workflow-loader": "0.1.0",
|
|
99
|
-
"@agentproto/workflow-runtime": "0.5.0",
|
|
100
|
-
"@agentproto/provider-kit": "0.3.0",
|
|
101
|
-
"@agentproto/provider-presets": "0.4.1",
|
|
102
|
-
"@agentproto/eval-reporters": "0.2.3",
|
|
103
114
|
"@agentproto/agent": "0.2.1",
|
|
115
|
+
"@agentproto/acp": "0.7.0",
|
|
116
|
+
"@agentproto/command-sandbox": "0.2.0",
|
|
117
|
+
"@agentproto/auth": "1.0.0",
|
|
118
|
+
"@agentproto/driver": "0.2.0",
|
|
119
|
+
"@agentproto/driver-agent-cli": "2.1.0",
|
|
120
|
+
"@agentproto/eval-reporters": "0.2.4",
|
|
104
121
|
"@agentproto/manifest": "0.2.1",
|
|
105
|
-
"@agentproto/mcp-server": "0.2.
|
|
106
|
-
"@agentproto/model-catalog": "0.
|
|
107
|
-
"@agentproto/
|
|
122
|
+
"@agentproto/mcp-server": "0.2.4",
|
|
123
|
+
"@agentproto/model-catalog": "0.7.0",
|
|
124
|
+
"@agentproto/provider-kit": "0.4.0",
|
|
125
|
+
"@agentproto/provider-presets": "0.5.0",
|
|
126
|
+
"@agentproto/providers-store": "0.3.2",
|
|
108
127
|
"@agentproto/redaction": "0.2.1",
|
|
109
|
-
"@agentproto/
|
|
110
|
-
"@agentproto/
|
|
111
|
-
"@agentproto/
|
|
128
|
+
"@agentproto/routine": "0.2.0",
|
|
129
|
+
"@agentproto/sandbox": "0.2.0",
|
|
130
|
+
"@agentproto/secrets": "0.2.2",
|
|
131
|
+
"@agentproto/telemetry-langfuse": "0.2.3",
|
|
132
|
+
"@agentproto/tool": "0.2.1",
|
|
133
|
+
"@agentproto/workflow-loader": "0.1.2",
|
|
134
|
+
"@agentproto/workflow": "0.1.1",
|
|
135
|
+
"@agentproto/workflow-runtime": "0.6.0"
|
|
112
136
|
},
|
|
113
137
|
"optionalDependencies": {
|
|
114
|
-
"@agentproto/sandbox-e2b": "0.
|
|
138
|
+
"@agentproto/sandbox-e2b": "0.3.0"
|
|
115
139
|
},
|
|
116
140
|
"devDependencies": {
|
|
117
141
|
"@types/node": "^25.6.2",
|
|
118
142
|
"@types/ws": "^8.5.13",
|
|
119
143
|
"tsup": "^8.5.1",
|
|
120
144
|
"typescript": "^5.9.3",
|
|
145
|
+
"vite-node": "^3.2.4",
|
|
121
146
|
"vitest": "^3.2.4",
|
|
122
|
-
"@agentproto/
|
|
123
|
-
"@agentproto/rendezvous": "0.2.0",
|
|
124
|
-
"@agentproto/tool": "0.2.1",
|
|
147
|
+
"@agentproto/rendezvous": "0.2.1",
|
|
125
148
|
"@agentproto/tooling": "0.1.0-alpha.0"
|
|
126
149
|
},
|
|
127
150
|
"scripts": {
|
|
@@ -130,6 +153,8 @@
|
|
|
130
153
|
"clean": "rm -rf dist",
|
|
131
154
|
"check-types": "tsc --noEmit",
|
|
132
155
|
"test": "vitest run --passWithNoTests",
|
|
133
|
-
"test:watch": "vitest"
|
|
156
|
+
"test:watch": "vitest",
|
|
157
|
+
"sim:inbound": "vite-node scripts/simulate-inbound.mjs",
|
|
158
|
+
"telegram:proxy": "vite-node scripts/telegram-proxy.mjs"
|
|
134
159
|
}
|
|
135
160
|
}
|