arisa 2.3.55 → 3.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.
Files changed (62) hide show
  1. package/AGENTS.md +102 -0
  2. package/README.md +120 -165
  3. package/cli/openai-transcribe/index.js +51 -0
  4. package/cli/openai-transcribe/package.json +6 -0
  5. package/cli/openai-transcribe/tool.manifest.json +15 -0
  6. package/cli/openai-tts/index.js +58 -0
  7. package/cli/openai-tts/package.json +6 -0
  8. package/cli/openai-tts/tool.manifest.json +20 -0
  9. package/cli/web-browser/index.js +146 -0
  10. package/cli/web-browser/package.json +6 -0
  11. package/cli/web-browser/tool.manifest.json +8 -0
  12. package/package.json +26 -44
  13. package/src/core/agent/agent-manager.js +218 -0
  14. package/src/core/artifacts/artifact-store.js +102 -0
  15. package/src/core/config/config-store.js +20 -0
  16. package/src/core/tools/tool-registry.js +117 -0
  17. package/src/index.js +27 -0
  18. package/src/runtime/bootstrap.js +213 -0
  19. package/src/runtime/create-app.js +22 -0
  20. package/src/transport/telegram/auth.js +13 -0
  21. package/src/transport/telegram/bot.js +214 -0
  22. package/src/transport/telegram/media.js +75 -0
  23. package/CLAUDE.md +0 -191
  24. package/SOUL.md +0 -36
  25. package/bin/arisa.js +0 -644
  26. package/scripts/dump-commands.ts +0 -26
  27. package/scripts/test-secrets.ts +0 -22
  28. package/src/core/attachments.ts +0 -104
  29. package/src/core/auth.ts +0 -58
  30. package/src/core/context.ts +0 -30
  31. package/src/core/file-detector.ts +0 -39
  32. package/src/core/format.ts +0 -159
  33. package/src/core/index.ts +0 -456
  34. package/src/core/intent.ts +0 -119
  35. package/src/core/media.ts +0 -144
  36. package/src/core/onboarding.ts +0 -102
  37. package/src/core/processor.ts +0 -305
  38. package/src/core/router.ts +0 -64
  39. package/src/core/scheduler.ts +0 -193
  40. package/src/daemon/agent-cli.ts +0 -130
  41. package/src/daemon/auto-install.ts +0 -158
  42. package/src/daemon/autofix.ts +0 -116
  43. package/src/daemon/bridge.ts +0 -166
  44. package/src/daemon/channels/base.ts +0 -10
  45. package/src/daemon/channels/telegram.ts +0 -306
  46. package/src/daemon/claude-login.ts +0 -218
  47. package/src/daemon/codex-login.ts +0 -172
  48. package/src/daemon/fallback.ts +0 -73
  49. package/src/daemon/index.ts +0 -272
  50. package/src/daemon/lifecycle.ts +0 -313
  51. package/src/daemon/setup.ts +0 -329
  52. package/src/shared/ai-cli.ts +0 -165
  53. package/src/shared/config.ts +0 -137
  54. package/src/shared/db.ts +0 -304
  55. package/src/shared/deepbase-secure.ts +0 -39
  56. package/src/shared/ink-shim.js +0 -14
  57. package/src/shared/logger.ts +0 -42
  58. package/src/shared/paths.ts +0 -90
  59. package/src/shared/ports.ts +0 -120
  60. package/src/shared/secrets.ts +0 -136
  61. package/src/shared/types.ts +0 -103
  62. package/tsconfig.json +0 -19
@@ -0,0 +1,146 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ function printHelp() {
4
+ console.log(`web-browser\n\nUsage:\n node index.js --help\n node index.js run --request-file <json>\n\nExpected input:\n {\n "text": "weather toronto" | "https://example.com",\n "artifact": { "text": "weather toronto" },\n "args": {\n "mode": "search" | "open",\n "url": "https://example.com",\n "maxResults": "5"\n }\n }\n\nBehavior:\n - If the input looks like a URL, open the page.\n - Otherwise, perform a web search.\n - When possible, opening pages uses r.jina.ai with a direct fetch fallback.\n`);
5
+ }
6
+
7
+ function decodeHtml(text = "") {
8
+ return text
9
+ .replace(/&amp;/g, "&")
10
+ .replace(/&quot;/g, '"')
11
+ .replace(/&#39;/g, "'")
12
+ .replace(/&lt;/g, "<")
13
+ .replace(/&gt;/g, ">")
14
+ .replace(/&nbsp;/g, " ");
15
+ }
16
+
17
+ function stripHtml(html = "") {
18
+ return decodeHtml(
19
+ html
20
+ .replace(/<script[\s\S]*?<\/script>/gi, " ")
21
+ .replace(/<style[\s\S]*?<\/style>/gi, " ")
22
+ .replace(/<[^>]+>/g, " ")
23
+ .replace(/\s+/g, " ")
24
+ ).trim();
25
+ }
26
+
27
+ function normalizeUrl(value = "") {
28
+ const text = value.trim();
29
+ if (!text) return "";
30
+ if (/^https?:\/\//i.test(text)) return text;
31
+ if (/^[\w.-]+\.[a-z]{2,}(\/|$)/i.test(text)) return `https://${text}`;
32
+ return "";
33
+ }
34
+
35
+ function extractActualUrl(duckUrl) {
36
+ try {
37
+ const parsed = new URL(duckUrl.startsWith("//") ? `https:${duckUrl}` : duckUrl);
38
+ const uddg = parsed.searchParams.get("uddg");
39
+ return uddg ? decodeURIComponent(uddg) : parsed.toString();
40
+ } catch {
41
+ return duckUrl;
42
+ }
43
+ }
44
+
45
+ async function fetchText(url) {
46
+ const response = await fetch(url, {
47
+ headers: {
48
+ "user-agent": "Mozilla/5.0",
49
+ "accept-language": "es-AR,es;q=0.9,en;q=0.8"
50
+ },
51
+ redirect: "follow"
52
+ });
53
+ return { response, text: await response.text() };
54
+ }
55
+
56
+ async function searchWeb(query, maxResults = 5) {
57
+ const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
58
+ const { response, text: html } = await fetchText(url);
59
+ if (!response.ok) throw new Error(`Search failed with status ${response.status}`);
60
+
61
+ const results = [];
62
+ const blocks = html.split(/<div class="result results_links[\s\S]*?web-result ">/i).slice(1);
63
+ for (const block of blocks) {
64
+ if (results.length >= maxResults) break;
65
+ const titleMatch = block.match(/<a[^>]*class="result__a"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/i);
66
+ if (!titleMatch) continue;
67
+ const snippetMatch = block.match(/<a[^>]*class="result__snippet"[^>]*>([\s\S]*?)<\/a>/i);
68
+ const displayUrlMatch = block.match(/<a[^>]*class="result__url"[^>]*>([\s\S]*?)<\/a>/i);
69
+ results.push({
70
+ title: stripHtml(titleMatch[2]),
71
+ url: extractActualUrl(titleMatch[1]),
72
+ snippet: stripHtml(snippetMatch?.[1] || ""),
73
+ displayUrl: stripHtml(displayUrlMatch?.[1] || "")
74
+ });
75
+ }
76
+
77
+ if (!results.length) {
78
+ return `Search: ${query}\n\nNo parseable results were found.`;
79
+ }
80
+
81
+ return [
82
+ `Search: ${query}`,
83
+ "",
84
+ ...results.flatMap((item, index) => [
85
+ `${index + 1}. ${item.title}`,
86
+ `URL: ${item.url}`,
87
+ `Snippet: ${item.snippet}`,
88
+ item.displayUrl ? `Displayed: ${item.displayUrl}` : null,
89
+ ""
90
+ ].filter(Boolean))
91
+ ].join("\n").trim();
92
+ }
93
+
94
+ async function openWebPage(inputUrl) {
95
+ const targetUrl = normalizeUrl(inputUrl);
96
+ if (!targetUrl) throw new Error("A valid URL is required");
97
+
98
+ const jinaUrl = `https://r.jina.ai/http://${targetUrl.replace(/^https?:\/\//i, "")}`;
99
+ let body = "";
100
+ let source = "jina-ai";
101
+
102
+ try {
103
+ const { response, text } = await fetchText(jinaUrl);
104
+ if (!response.ok) throw new Error(`r.jina.ai status ${response.status}`);
105
+ body = text.trim();
106
+ } catch {
107
+ const { response, text } = await fetchText(targetUrl);
108
+ if (!response.ok) throw new Error(`Open failed with status ${response.status}`);
109
+ body = stripHtml(text);
110
+ source = "direct-fetch";
111
+ }
112
+
113
+ const shortened = body.length > 12000 ? `${body.slice(0, 12000)}\n\n[content truncated]` : body;
114
+ return [`Page: ${targetUrl}`, `Source: ${source}`, "", shortened].join("\n").trim();
115
+ }
116
+
117
+ async function run(requestFile) {
118
+ const request = JSON.parse(await readFile(requestFile, "utf8"));
119
+ const rawInput = request.args?.url || request.text || request.artifact?.text || "";
120
+ const mode = request.args?.mode || (normalizeUrl(rawInput) ? "open" : "search");
121
+ const maxResults = Number.parseInt(request.args?.maxResults || "5", 10);
122
+
123
+ if (!rawInput.trim()) {
124
+ console.log(JSON.stringify({ ok: false, error: "text, artifact.text, or args.url is required" }));
125
+ return;
126
+ }
127
+
128
+ try {
129
+ const outputText = mode === "open"
130
+ ? await openWebPage(rawInput)
131
+ : await searchWeb(rawInput, Number.isFinite(maxResults) ? maxResults : 5);
132
+ console.log(JSON.stringify({ ok: true, output: { text: outputText } }));
133
+ } catch (error) {
134
+ console.log(JSON.stringify({ ok: false, error: error.message || String(error) }));
135
+ }
136
+ }
137
+
138
+ const args = process.argv.slice(2);
139
+ if (!args.length || args.includes("--help") || args[0] === "help") {
140
+ printHelp();
141
+ } else if (args[0] === "run") {
142
+ const fileIndex = args.indexOf("--request-file");
143
+ await run(args[fileIndex + 1]);
144
+ } else {
145
+ printHelp();
146
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "web-browser-cli",
3
+ "private": true,
4
+ "type": "module",
5
+ "version": "1.0.0"
6
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "web-browser",
3
+ "description": "Search the web and open web pages as readable text.",
4
+ "entry": "index.js",
5
+ "input": ["text/plain"],
6
+ "output": ["text/plain"],
7
+ "configSchema": {}
8
+ }
package/package.json CHANGED
@@ -1,55 +1,37 @@
1
1
  {
2
2
  "name": "arisa",
3
- "version": "2.3.55",
4
- "description": "Arisa - dynamic agent runtime with daemon/core architecture that evolves through user interaction",
3
+ "version": "3.0.0",
4
+ "description": "Telegram + Pi Agent modular assistant",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "arisa": "./src/index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node src/index.js",
12
+ "bootstrap": "node src/index.js --bootstrap"
13
+ },
5
14
  "keywords": [
6
- "tinyclaw",
7
- "clawbot",
8
- "moltbot",
15
+ "telegram",
16
+ "pi-agent",
17
+ "assistant",
18
+ "cli",
9
19
  "openclaw",
20
+ "moltbot",
21
+ "clawbot",
10
22
  "agent",
11
- "telegram",
12
- "jarvis",
13
- "CLAUDE.md",
14
23
  "SOUL.md",
24
+ "tinyclaw",
25
+ "jarvis",
26
+ "AGENTS.md",
15
27
  "clasen"
16
28
  ],
17
- "preferGlobal": true,
18
- "bin": {
19
- "arisa": "./bin/arisa.js"
20
- },
21
- "files": [
22
- "bin",
23
- "src",
24
- "scripts",
25
- "CLAUDE.md",
26
- "SOUL.md",
27
- "README.md",
28
- "tsconfig.json"
29
- ],
30
- "engines": {
31
- "node": ">=18",
32
- "bun": ">=1.0.0"
33
- },
34
- "scripts": {
35
- "arisa": "bun ./bin/arisa.js",
36
- "daemon": "bun src/daemon/index.ts",
37
- "dev": "bun --watch src/core/index.ts",
38
- "start": "bun src/daemon/index.ts",
39
- "core": "bun src/core/index.ts"
40
- },
29
+ "author": "",
30
+ "license": "ISC",
31
+ "packageManager": "pnpm@10.32.1",
41
32
  "dependencies": {
42
- "@inquirer/prompts": "^8.2.0",
43
- "croner": "^9.0.0",
44
- "crypto-js": "^4.2.0",
45
- "deepbase": "^3.4.9",
46
- "elevenlabs": "^1.59.0",
47
- "grammy": "^1.21.0",
48
- "openai": "^6.19.0",
49
- "sharp": "^0.34.5"
50
- },
51
- "devDependencies": {
52
- "@types/bun": "latest",
53
- "@types/crypto-js": "^4.2.2"
33
+ "@mariozechner/pi-coding-agent": "^0.65.0",
34
+ "@sinclair/typebox": "^0.34.41",
35
+ "grammy": "^1.42.0"
54
36
  }
55
37
  }
@@ -0,0 +1,218 @@
1
+ import path from "node:path";
2
+ import { mkdir } from "node:fs/promises";
3
+ import { AuthStorage, createAgentSession, ModelRegistry, SessionManager, defineTool } from "@mariozechner/pi-coding-agent";
4
+ import { Type } from "@sinclair/typebox";
5
+
6
+ const agentDir = path.resolve("data/pi-agent");
7
+
8
+ function getOAuthProviderForModelProvider(provider) {
9
+ if (provider === "openai-codex") return "openai-codex";
10
+ if (provider === "anthropic") return "anthropic";
11
+ if (provider === "google") return "google-gemini-cli";
12
+ if (provider === "google-antigravity") return "google-antigravity";
13
+ if (provider === "github-copilot") return "github-copilot";
14
+ return provider;
15
+ }
16
+
17
+ function normalizePiConfig(pi) {
18
+ const provider = pi.provider === "codex" ? "openai" : pi.provider;
19
+ let model = pi.model;
20
+ if (pi.provider === "codex") {
21
+ if (model === "5.4") model = "gpt-5.4";
22
+ else if (model === "5.4-mini") model = "gpt-5.4-mini";
23
+ else if (model === "5.4-nano") model = "gpt-5.4-nano";
24
+ else if (model === "5.4-pro") model = "gpt-5.4-pro";
25
+ else if (!model.startsWith("gpt-")) model = `gpt-${model}`;
26
+ }
27
+ return { provider, model };
28
+ }
29
+
30
+ export class AgentManager {
31
+ constructor({ config, artifactStore, toolRegistry }) {
32
+ this.config = config;
33
+ this.artifactStore = artifactStore;
34
+ this.toolRegistry = toolRegistry;
35
+ this.sessions = new Map();
36
+ }
37
+
38
+ setConfig(config) {
39
+ this.config = config;
40
+ this.sessions.clear();
41
+ }
42
+
43
+ async validatePiAgent() {
44
+ const authStorage = AuthStorage.create();
45
+ const normalized = normalizePiConfig(this.config.pi);
46
+ if (this.config.pi.apiKey) {
47
+ authStorage.setRuntimeApiKey(normalized.provider, this.config.pi.apiKey);
48
+ }
49
+
50
+ const modelRegistry = ModelRegistry.create(authStorage);
51
+ const model = modelRegistry.find(normalized.provider, normalized.model);
52
+ if (!model) {
53
+ throw new Error(`Model not found: ${this.config.pi.provider}/${this.config.pi.model} (resolved to ${normalized.provider}/${normalized.model})`);
54
+ }
55
+ const oauthProvider = getOAuthProviderForModelProvider(normalized.provider);
56
+ if (!this.config.pi.apiKey && !modelRegistry.hasConfiguredAuth(normalized.provider) && !authStorage.hasAuth(oauthProvider)) {
57
+ throw new Error(`No auth found for ${normalized.provider}. Provide a Pi API key in bootstrap, or authenticate with the internal /login flow during bootstrap.`);
58
+ }
59
+
60
+ const { session } = await createAgentSession({
61
+ authStorage,
62
+ modelRegistry,
63
+ model,
64
+ sessionManager: SessionManager.inMemory(),
65
+ });
66
+ await session.prompt("Reply with exactly: OK");
67
+ }
68
+
69
+ async getSessionContext(chatId, telegram) {
70
+ if (this.sessions.has(chatId)) return this.sessions.get(chatId);
71
+
72
+ await mkdir(agentDir, { recursive: true });
73
+ const authStorage = AuthStorage.create();
74
+ const normalized = normalizePiConfig(this.config.pi);
75
+ if (this.config.pi.apiKey) {
76
+ authStorage.setRuntimeApiKey(normalized.provider, this.config.pi.apiKey);
77
+ }
78
+ const modelRegistry = ModelRegistry.create(authStorage);
79
+ const model = modelRegistry.find(normalized.provider, normalized.model);
80
+ if (!model) throw new Error(`Model not found: ${this.config.pi.provider}/${this.config.pi.model} (resolved to ${normalized.provider}/${normalized.model})`);
81
+ const oauthProvider = getOAuthProviderForModelProvider(normalized.provider);
82
+ if (!this.config.pi.apiKey && !modelRegistry.hasConfiguredAuth(normalized.provider) && !authStorage.hasAuth(oauthProvider)) {
83
+ throw new Error(`No auth found for ${normalized.provider}. Re-run bootstrap and complete login for this provider before Telegram starts.`);
84
+ }
85
+
86
+ const cwd = path.resolve("data/chats", String(chatId));
87
+ await mkdir(cwd, { recursive: true });
88
+
89
+ const customTools = this.createTools(telegram);
90
+ const { session } = await createAgentSession({
91
+ cwd,
92
+ agentDir,
93
+ authStorage,
94
+ modelRegistry,
95
+ model,
96
+ customTools,
97
+ sessionManager: SessionManager.continueRecent(cwd)
98
+ });
99
+
100
+ const ctx = { session };
101
+ this.sessions.set(chatId, ctx);
102
+ return ctx;
103
+ }
104
+
105
+ createTools(telegram) {
106
+ return [
107
+ defineTool({
108
+ name: "list_tools",
109
+ label: "List tools",
110
+ description: "List registered CLI tools and their capabilities.",
111
+ parameters: Type.Object({}),
112
+ execute: async () => {
113
+ await this.toolRegistry.load();
114
+ return {
115
+ content: [{ type: "text", text: JSON.stringify(this.toolRegistry.list(), null, 2) }],
116
+ details: { tools: this.toolRegistry.list() }
117
+ };
118
+ }
119
+ }),
120
+ defineTool({
121
+ name: "tool_help",
122
+ label: "Tool help",
123
+ description: "Show --help text for a CLI tool.",
124
+ parameters: Type.Object({ name: Type.String() }),
125
+ execute: async (_id, params) => {
126
+ await this.toolRegistry.load();
127
+ const help = await this.toolRegistry.help(params.name);
128
+ return { content: [{ type: "text", text: help }], details: { help } };
129
+ }
130
+ }),
131
+ defineTool({
132
+ name: "set_tool_config",
133
+ label: "Set tool config",
134
+ description: "Write a value into cli/<tool>/config.js.",
135
+ parameters: Type.Object({ name: Type.String(), field: Type.String(), value: Type.String() }),
136
+ execute: async (_id, params) => {
137
+ await this.toolRegistry.load();
138
+ const result = await this.toolRegistry.setConfig(params.name, params.field, params.value);
139
+ return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
140
+ }
141
+ }),
142
+ defineTool({
143
+ name: "run_tool",
144
+ label: "Run tool",
145
+ description: "Run a CLI tool using text input or an artifactId. If config is missing, ask the user naturally and then use set_tool_config.",
146
+ parameters: Type.Object({
147
+ name: Type.String(),
148
+ artifactId: Type.Optional(Type.String()),
149
+ text: Type.Optional(Type.String()),
150
+ args: Type.Optional(Type.Record(Type.String(), Type.String()))
151
+ }),
152
+ execute: async (_id, params) => {
153
+ await this.toolRegistry.load();
154
+ let artifact = null;
155
+ if (params.artifactId) {
156
+ artifact = await this.artifactStore.get(params.artifactId);
157
+ if (!artifact) {
158
+ return { content: [{ type: "text", text: `Artifact not found: ${params.artifactId}` }], details: { ok: false } };
159
+ }
160
+ }
161
+ const result = await this.toolRegistry.run({
162
+ name: params.name,
163
+ request: {
164
+ artifact,
165
+ text: params.text,
166
+ args: params.args || {}
167
+ }
168
+ });
169
+
170
+ if (result.output?.text) {
171
+ const outArtifact = await this.artifactStore.createText({
172
+ text: result.output.text,
173
+ source: { type: "tool", toolName: params.name },
174
+ metadata: { tool: params.name }
175
+ });
176
+ result.output.artifactId = outArtifact.id;
177
+ }
178
+
179
+ if (result.output?.filePath) {
180
+ const generated = await this.artifactStore.createFromFile({
181
+ originalPath: result.output.filePath,
182
+ fileName: result.output.fileName || path.basename(result.output.filePath),
183
+ kind: result.output.kind || "file",
184
+ mimeType: result.output.mimeType || "application/octet-stream",
185
+ source: { type: "tool", toolName: params.name },
186
+ metadata: { tool: params.name }
187
+ });
188
+ result.output.artifactId = generated.id;
189
+ }
190
+
191
+ return {
192
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
193
+ details: result
194
+ };
195
+ }
196
+ }),
197
+ defineTool({
198
+ name: "send_audio_reply",
199
+ label: "Send audio reply",
200
+ description: "Generate speech from text with a CLI tool and send it to the current Telegram chat.",
201
+ parameters: Type.Object({ text: Type.String(), toolName: Type.Optional(Type.String()) }),
202
+ execute: async (_id, params) => {
203
+ await this.toolRegistry.load();
204
+ const toolName = params.toolName || "openai-tts";
205
+ const result = await this.toolRegistry.run({
206
+ name: toolName,
207
+ request: { text: params.text, args: {} }
208
+ });
209
+ if (!result.ok || !result.output?.filePath) {
210
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
211
+ }
212
+ await telegram.sendAudio(result.output.filePath, params.text);
213
+ return { content: [{ type: "text", text: "Audio enviado por Telegram." }], details: result };
214
+ }
215
+ })
216
+ ];
217
+ }
218
+ }
@@ -0,0 +1,102 @@
1
+ import { mkdir, readFile, writeFile, copyFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import crypto from "node:crypto";
4
+
5
+ const rootDir = path.resolve("data/artifacts");
6
+ const indexFile = path.resolve("data/state/artifacts.json");
7
+
8
+ async function loadIndex() {
9
+ try {
10
+ return JSON.parse(await readFile(indexFile, "utf8"));
11
+ } catch {
12
+ return [];
13
+ }
14
+ }
15
+
16
+ async function saveIndex(items) {
17
+ await mkdir(path.dirname(indexFile), { recursive: true });
18
+ await writeFile(indexFile, `${JSON.stringify(items, null, 2)}\n`, "utf8");
19
+ }
20
+
21
+ function id() {
22
+ return crypto.randomUUID();
23
+ }
24
+
25
+ export class ArtifactStore {
26
+ constructor() {
27
+ this.items = null;
28
+ }
29
+
30
+ async init() {
31
+ if (!this.items) this.items = await loadIndex();
32
+ await mkdir(rootDir, { recursive: true });
33
+ }
34
+
35
+ async createText({ text, mimeType = "text/plain", source, metadata = {} }) {
36
+ await this.init();
37
+ const artifact = {
38
+ id: id(),
39
+ kind: "text",
40
+ mimeType,
41
+ text,
42
+ source,
43
+ metadata,
44
+ createdAt: new Date().toISOString()
45
+ };
46
+ this.items.push(artifact);
47
+ await saveIndex(this.items);
48
+ return artifact;
49
+ }
50
+
51
+ async createFromFile({ originalPath, fileName, kind, mimeType, source, metadata = {} }) {
52
+ await this.init();
53
+ const artifactId = id();
54
+ const dir = path.join(rootDir, artifactId);
55
+ await mkdir(dir, { recursive: true });
56
+ const destPath = path.join(dir, fileName);
57
+ await copyFile(originalPath, destPath);
58
+ const artifact = {
59
+ id: artifactId,
60
+ kind,
61
+ mimeType,
62
+ path: destPath,
63
+ source,
64
+ metadata,
65
+ createdAt: new Date().toISOString()
66
+ };
67
+ this.items.push(artifact);
68
+ await saveIndex(this.items);
69
+ return artifact;
70
+ }
71
+
72
+ async createGeneratedFile({ fileName, content, kind, mimeType, source, metadata = {} }) {
73
+ await this.init();
74
+ const artifactId = id();
75
+ const dir = path.join(rootDir, artifactId);
76
+ await mkdir(dir, { recursive: true });
77
+ const destPath = path.join(dir, fileName);
78
+ await writeFile(destPath, content);
79
+ const artifact = {
80
+ id: artifactId,
81
+ kind,
82
+ mimeType,
83
+ path: destPath,
84
+ source,
85
+ metadata,
86
+ createdAt: new Date().toISOString()
87
+ };
88
+ this.items.push(artifact);
89
+ await saveIndex(this.items);
90
+ return artifact;
91
+ }
92
+
93
+ async get(id) {
94
+ await this.init();
95
+ return this.items.find((item) => item.id === id) || null;
96
+ }
97
+
98
+ async listRecent(limit = 20) {
99
+ await this.init();
100
+ return [...this.items].slice(-limit).reverse();
101
+ }
102
+ }
@@ -0,0 +1,20 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { configFile } from "../../runtime/bootstrap.js";
4
+
5
+ export async function loadConfig() {
6
+ const raw = await readFile(configFile, "utf8");
7
+ return JSON.parse(raw);
8
+ }
9
+
10
+ export async function saveConfig(config) {
11
+ await mkdir(path.dirname(configFile), { recursive: true });
12
+ await writeFile(configFile, `${JSON.stringify(config, null, 2)}\n`, "utf8");
13
+ }
14
+
15
+ export async function updateConfig(mutator) {
16
+ const config = await loadConfig();
17
+ await mutator(config);
18
+ await saveConfig(config);
19
+ return config;
20
+ }