arisa 3.0.6 → 3.0.9
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/AGENTS.md +27 -33
- package/README.md +15 -10
- package/package.json +1 -1
- package/src/core/agent/agent-manager.js +9 -10
- package/src/core/agent/runtime-context.js +16 -0
- package/src/core/tools/tool-registry.js +38 -29
- package/src/index.js +23 -0
- package/src/runtime/paths.js +1 -9
- package/src/runtime/pi-package-manager.js +49 -0
package/AGENTS.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Arisa AGENTS
|
|
2
2
|
|
|
3
3
|
## Architecture
|
|
4
|
-
-
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
4
|
+
- Telegram transport handles inbound and outbound messaging.
|
|
5
|
+
- Pi Agent keeps one session per authorized chat.
|
|
6
|
+
- Every incoming or generated message or file becomes an artifact.
|
|
7
|
+
- A tool registry handles tool discovery, help lookup, config writes, and execution.
|
|
8
|
+
- Tools are isolated and each one has its own manifest, entrypoint, and config defaults.
|
|
9
9
|
|
|
10
10
|
## Main rule: everything is piped through artifacts
|
|
11
11
|
A pipe transforms one input artifact into one output artifact.
|
|
@@ -70,45 +70,39 @@ Example manual pipe:
|
|
|
70
70
|
## Missing config flow
|
|
71
71
|
If `run_tool` returns `missingConfig`, the agent should:
|
|
72
72
|
1. ask the user naturally in Telegram for the missing value
|
|
73
|
-
2. write the value
|
|
73
|
+
2. write the value with `set_tool_config`
|
|
74
74
|
3. retry the tool
|
|
75
75
|
|
|
76
76
|
Do not assume a rigid question/answer protocol. Continue the conversation naturally and infer the config value from the user reply when possible.
|
|
77
77
|
|
|
78
|
-
##
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
The acknowledgment should:
|
|
82
|
-
- be short and clear
|
|
83
|
-
- tell the user the work is starting
|
|
84
|
-
- mention when the task may take a while
|
|
78
|
+
## Tool creation
|
|
79
|
+
Reason in terms of capabilities, not tool names.
|
|
85
80
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
81
|
+
When the user asks for something new:
|
|
82
|
+
1. check whether an existing registered tool can already satisfy the task
|
|
83
|
+
2. also check whether the task can be satisfied indirectly through an existing capability
|
|
84
|
+
3. only propose creating a new tool when the needed capability is truly missing
|
|
89
85
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
If the user asks for a capability that is not currently available, first check whether an existing registered tool can satisfy the task.
|
|
93
|
-
If no existing tool can do it, the default attitude should be to propose creating a new CLI tool under `tools/<tool-name>` following the project conventions.
|
|
94
|
-
All newly created tools must document their help text, usage instructions, manifests, and user-facing operational strings in English.
|
|
95
|
-
Do not stop at "I cannot do that" when the task is realistically implementable through a new tool.
|
|
96
|
-
Prefer responses like:
|
|
86
|
+
Do not stop at "I cannot do that" when the task is realistically implementable through the tool architecture.
|
|
87
|
+
The default attitude is:
|
|
97
88
|
- identify that no current tool satisfies the request
|
|
98
89
|
- state that the missing capability can be added
|
|
99
|
-
- propose or start creating the
|
|
100
|
-
|
|
101
|
-
For example, if the user asks for live weather and no weather tool exists, the correct attitude is to propose building a weather tool for the bot rather than only saying real-time access is unavailable.
|
|
90
|
+
- propose or start creating the needed tool
|
|
102
91
|
|
|
103
|
-
When creating or editing tools
|
|
104
|
-
-
|
|
105
|
-
-
|
|
106
|
-
-
|
|
92
|
+
When creating or editing tools:
|
|
93
|
+
- use the shared path helpers and the runtime paths provided in the prompt instead of assuming fixed locations
|
|
94
|
+
- consult the local skill for that workflow when building new tools
|
|
95
|
+
- keep all help text, usage instructions, manifests, and user-facing operational strings in English
|
|
96
|
+
- follow the One Thing Rule: each function or method should do one thing well; if it mixes low-level operations with high-level policy, split it into smaller focused units
|
|
107
97
|
|
|
108
|
-
|
|
98
|
+
## Dependency installation
|
|
99
|
+
Arisa installs tool dependencies itself.
|
|
100
|
+
- Prefer `pnpm install`.
|
|
101
|
+
- Fall back to `npm install`.
|
|
102
|
+
- Do not ask the user to do it manually.
|
|
109
103
|
|
|
110
104
|
## Safety
|
|
111
|
-
- Do not install or run arbitrary tools outside registered
|
|
105
|
+
- Do not install or run arbitrary tools outside registered tool manifests in V1.
|
|
112
106
|
- Prefer tool manifests and CLI help over assumptions.
|
|
113
|
-
- Keep tool
|
|
107
|
+
- Keep tool config and runtime data inside the user runtime area.
|
|
114
108
|
- Be proactive about extending capabilities, but do it through the project's tool architecture, not through ad hoc one-off behavior.
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Arisa is a personal Telegram assistant powered by Pi Agent.
|
|
|
4
4
|
|
|
5
5
|
## Origin
|
|
6
6
|
|
|
7
|
-
The initial inspiration was [OpenClaw](https://github.com/openclaw/openclaw). OpenClaw has interesting ideas but carries
|
|
7
|
+
The initial inspiration was [OpenClaw](https://github.com/openclaw/openclaw). OpenClaw has interesting ideas but carries a lot of weight (about **185 MB**) compared to Arisa (**76.7 kB**): when it generates tools they end up disorganized, and the overall framework feels overloaded.
|
|
8
8
|
|
|
9
9
|
The real heart of OpenClaw is Pi Agent: a [minimal terminal coding harness](https://www.youtube.com/watch?v=Dli5slNaJu0) that lets an AI agent reason and act with very little infrastructure. That part is genuinely good.
|
|
10
10
|
|
|
@@ -46,7 +46,9 @@ This distinction is important. Some transformations belong to the transport/inpu
|
|
|
46
46
|
- media is stored as artifacts
|
|
47
47
|
|
|
48
48
|
### Tool model
|
|
49
|
-
|
|
49
|
+
Bundled tools live under `<arisa-install-dir>/tools/<tool-name>` and user-created tools live under `~/.arisa/tools/<tool-name>`.
|
|
50
|
+
|
|
51
|
+
Each tool folder contains:
|
|
50
52
|
|
|
51
53
|
- `package.json`
|
|
52
54
|
- `config.js`
|
|
@@ -57,7 +59,8 @@ Each tool is isolated from the root project and from other tools.
|
|
|
57
59
|
That isolation is part of the architecture:
|
|
58
60
|
|
|
59
61
|
- each tool has its own folder
|
|
60
|
-
-
|
|
62
|
+
- bundled tools have a local `config.js` for defaults/template values
|
|
63
|
+
- user-created tools can live entirely inside `~/.arisa/tools/<tool>/`
|
|
61
64
|
- each tool can have its own dependencies
|
|
62
65
|
- one tool can be changed or replaced without tightly coupling the rest of the system
|
|
63
66
|
|
|
@@ -74,8 +77,8 @@ node index.js run --request-file <json>
|
|
|
74
77
|
- artifact index is stored in `~/.arisa/state/artifacts.json`
|
|
75
78
|
- incoming Telegram attachments are stored directly in `~/.arisa/artifacts/`
|
|
76
79
|
- tool-specific secrets/config live in `~/.arisa/tools/<tool>/config.js`
|
|
77
|
-
-
|
|
78
|
-
- tool runtime temp files and generated outputs live
|
|
80
|
+
- user-created tools also live under `~/.arisa/tools/<tool>/`
|
|
81
|
+
- tool runtime temp files and generated outputs live under `~/.arisa/tools/<tool>/` (for example `tmp/` and `out/`)
|
|
79
82
|
- durable files should end up in `~/.arisa/artifacts/`
|
|
80
83
|
- Pi authentication can use either:
|
|
81
84
|
- an API key entered during bootstrap
|
|
@@ -96,11 +99,13 @@ arisa
|
|
|
96
99
|
Command modes:
|
|
97
100
|
|
|
98
101
|
```bash
|
|
99
|
-
arisa
|
|
100
|
-
arisa start
|
|
101
|
-
arisa stop
|
|
102
|
-
arisa status
|
|
103
|
-
arisa flush
|
|
102
|
+
arisa # foreground, blocking
|
|
103
|
+
arisa start # start in background
|
|
104
|
+
arisa stop # stop background service
|
|
105
|
+
arisa status # show background service status
|
|
106
|
+
arisa flush # remove ~/.arisa
|
|
107
|
+
arisa install <source> # install a Pi package into Arisa's runtime
|
|
108
|
+
arisa remove <source> # remove a Pi package from Arisa's runtime
|
|
104
109
|
```
|
|
105
110
|
|
|
106
111
|
## Bootstrap flow
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
3
|
import { createAgentSession, SessionManager, defineTool } from "@mariozechner/pi-coding-agent";
|
|
4
4
|
import { Type } from "@sinclair/typebox";
|
|
5
5
|
import { createPiRuntime, hasProviderAuth } from "./pi-runtime.js";
|
|
6
6
|
import { loadProjectInstructions } from "./project-instructions.js";
|
|
7
|
-
import {
|
|
7
|
+
import { arisaInstallDir, buildAgentRuntimeContext } from "./runtime-context.js";
|
|
8
|
+
import { arisaHomeDir } from "../../runtime/paths.js";
|
|
8
9
|
|
|
9
10
|
export class AgentManager {
|
|
10
11
|
constructor({ config, artifactStore, toolRegistry, logger }) {
|
|
@@ -49,7 +50,6 @@ export class AgentManager {
|
|
|
49
50
|
return this.sessions.get(chatId);
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
await mkdir(agentDir, { recursive: true });
|
|
53
53
|
const { authStorage, modelRegistry } = createPiRuntime({
|
|
54
54
|
provider: this.config.pi.provider,
|
|
55
55
|
apiKey: this.config.pi.apiKey
|
|
@@ -60,24 +60,23 @@ export class AgentManager {
|
|
|
60
60
|
throw new Error(`No auth found for ${this.config.pi.provider}. Re-run bootstrap and complete login for this provider before Telegram starts.`);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
const cwd = getChatDir(chatId);
|
|
64
|
-
await mkdir(cwd, { recursive: true });
|
|
65
|
-
|
|
66
63
|
this.logger?.log("agent", `creating session for chat ${chatId}`);
|
|
67
64
|
const customTools = this.createTools(telegram);
|
|
68
65
|
const { session } = await createAgentSession({
|
|
69
|
-
cwd,
|
|
70
|
-
agentDir,
|
|
66
|
+
cwd: arisaInstallDir,
|
|
67
|
+
agentDir: arisaHomeDir,
|
|
71
68
|
authStorage,
|
|
72
69
|
modelRegistry,
|
|
73
70
|
model,
|
|
74
71
|
customTools,
|
|
75
|
-
sessionManager: SessionManager.
|
|
72
|
+
sessionManager: SessionManager.inMemory()
|
|
76
73
|
});
|
|
77
74
|
|
|
78
75
|
const instructions = await loadProjectInstructions();
|
|
76
|
+
const runtimeContext = buildAgentRuntimeContext();
|
|
79
77
|
this.logger?.log("agent", `injecting project instructions for chat ${chatId}`);
|
|
80
|
-
|
|
78
|
+
this.logger?.log("agent", `runtime context for chat ${chatId}:\n${runtimeContext}`);
|
|
79
|
+
await session.prompt(`${instructions}\n\n${runtimeContext}\n\nAcknowledge with exactly: OK`);
|
|
81
80
|
|
|
82
81
|
const ctx = { session };
|
|
83
82
|
this.sessions.set(chatId, ctx);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { arisaHomeDir, artifactsDir, stateDir, toolsDir } from "../../runtime/paths.js";
|
|
3
|
+
|
|
4
|
+
export const arisaInstallDir = fileURLToPath(new URL("../../..", import.meta.url));
|
|
5
|
+
export const bundledToolsDir = fileURLToPath(new URL("../../../tools", import.meta.url));
|
|
6
|
+
|
|
7
|
+
export function buildAgentRuntimeContext() {
|
|
8
|
+
return [
|
|
9
|
+
`arisaHomeDir: ${arisaHomeDir}`,
|
|
10
|
+
`arisaInstallDir: ${arisaInstallDir}`,
|
|
11
|
+
`bundledToolsDir: ${bundledToolsDir}`,
|
|
12
|
+
`userToolsDir: ${toolsDir}`,
|
|
13
|
+
`artifactsDir: ${artifactsDir}`,
|
|
14
|
+
`stateDir: ${stateDir}`
|
|
15
|
+
].join("\n");
|
|
16
|
+
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { mkdir, readdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
|
-
import {
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { getToolConfigPath, getToolTmpDir, toolsDir as userToolsRoot } from "../../runtime/paths.js";
|
|
5
6
|
import { loadToolConfig, parseConfigModule, writeToolConfig } from "./tool-config.js";
|
|
6
7
|
|
|
7
|
-
const
|
|
8
|
+
const bundledToolsRoot = fileURLToPath(new URL("../../../tools", import.meta.url));
|
|
9
|
+
const toolRoots = [
|
|
10
|
+
{ root: userToolsRoot, kind: "user" },
|
|
11
|
+
{ root: bundledToolsRoot, kind: "bundled" }
|
|
12
|
+
];
|
|
8
13
|
|
|
9
14
|
function runProcess(command, args, options = {}) {
|
|
10
15
|
return new Promise((resolve) => {
|
|
@@ -26,35 +31,39 @@ export class ToolRegistry {
|
|
|
26
31
|
async load() {
|
|
27
32
|
this.tools.clear();
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
entries = await readdir(toolsRoot, { withFileTypes: true });
|
|
32
|
-
} catch {
|
|
33
|
-
this.logger?.log("tools", `tools directory not found: ${toolsRoot}`);
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
for (const entry of entries) {
|
|
38
|
-
if (!entry.isDirectory()) continue;
|
|
39
|
-
const toolDir = path.join(toolsRoot, entry.name);
|
|
40
|
-
const manifestPath = path.join(toolDir, "tool.manifest.json");
|
|
41
|
-
const configPath = path.join(toolDir, "config.js");
|
|
34
|
+
for (const { root, kind } of toolRoots) {
|
|
35
|
+
let entries = [];
|
|
42
36
|
try {
|
|
43
|
-
|
|
44
|
-
const configSource = await readFile(configPath, "utf8");
|
|
45
|
-
const defaults = parseConfigModule(configSource);
|
|
46
|
-
const config = await loadToolConfig(manifest.name, defaults);
|
|
47
|
-
this.tools.set(manifest.name, {
|
|
48
|
-
...manifest,
|
|
49
|
-
dir: toolDir,
|
|
50
|
-
entry: path.join(toolDir, manifest.entry || "index.js"),
|
|
51
|
-
localConfigPath: configPath,
|
|
52
|
-
configPath: getToolConfigPath(manifest.name),
|
|
53
|
-
defaults,
|
|
54
|
-
config
|
|
55
|
-
});
|
|
37
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
56
38
|
} catch {
|
|
57
|
-
|
|
39
|
+
this.logger?.log("tools", `${kind} tools directory not found: ${root}`);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
if (!entry.isDirectory()) continue;
|
|
45
|
+
const toolDir = path.join(root, entry.name);
|
|
46
|
+
const manifestPath = path.join(toolDir, "tool.manifest.json");
|
|
47
|
+
const configPath = path.join(toolDir, "config.js");
|
|
48
|
+
try {
|
|
49
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
50
|
+
if (this.tools.has(manifest.name)) continue;
|
|
51
|
+
const configSource = await readFile(configPath, "utf8");
|
|
52
|
+
const defaults = parseConfigModule(configSource);
|
|
53
|
+
const config = await loadToolConfig(manifest.name, defaults);
|
|
54
|
+
this.tools.set(manifest.name, {
|
|
55
|
+
...manifest,
|
|
56
|
+
dir: toolDir,
|
|
57
|
+
entry: path.join(toolDir, manifest.entry || "index.js"),
|
|
58
|
+
localConfigPath: configPath,
|
|
59
|
+
configPath: getToolConfigPath(manifest.name),
|
|
60
|
+
defaults,
|
|
61
|
+
config,
|
|
62
|
+
sourceKind: kind
|
|
63
|
+
});
|
|
64
|
+
} catch {
|
|
65
|
+
// ignore invalid tool dirs in v1
|
|
66
|
+
}
|
|
58
67
|
}
|
|
59
68
|
}
|
|
60
69
|
|
package/src/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createApp } from "./runtime/create-app.js";
|
|
|
5
5
|
import { createLogger } from "./runtime/logger.js";
|
|
6
6
|
import { getServiceStatus, registerServiceProcess, startService, stopService } from "./runtime/service-manager.js";
|
|
7
7
|
import { flushArisaHome } from "./runtime/flush.js";
|
|
8
|
+
import { installPiPackage, removePiPackage } from "./runtime/pi-package-manager.js";
|
|
8
9
|
|
|
9
10
|
const args = process.argv.slice(2);
|
|
10
11
|
const command = args.find((arg) => !arg.startsWith("--")) || "run";
|
|
@@ -83,6 +84,28 @@ async function main() {
|
|
|
83
84
|
return;
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
if (command === "install") {
|
|
88
|
+
const source = args.filter((arg) => !arg.startsWith("--")).slice(1)[0];
|
|
89
|
+
if (!source) {
|
|
90
|
+
console.log("Usage: arisa install <pi-package-source>");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const result = await installPiPackage(source);
|
|
94
|
+
process.exitCode = result.ok ? 0 : result.code;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (command === "remove") {
|
|
99
|
+
const source = args.filter((arg) => !arg.startsWith("--")).slice(1)[0];
|
|
100
|
+
if (!source) {
|
|
101
|
+
console.log("Usage: arisa remove <pi-package-source>");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const result = await removePiPackage(source);
|
|
105
|
+
process.exitCode = result.ok ? 0 : result.code;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
86
109
|
await runForeground();
|
|
87
110
|
}
|
|
88
111
|
|
package/src/runtime/paths.js
CHANGED
|
@@ -9,15 +9,9 @@ export const servicePidFile = path.join(stateDir, "arisa.pid");
|
|
|
9
9
|
export const serviceLogFile = path.join(stateDir, "arisa.log");
|
|
10
10
|
export const artifactsDir = path.join(arisaHomeDir, "artifacts");
|
|
11
11
|
export const artifactsIndexFile = path.join(stateDir, "artifacts.json");
|
|
12
|
-
export const piAgentDir = path.join(stateDir, "pi-agent");
|
|
13
|
-
export const chatsDir = path.join(stateDir, "chats");
|
|
14
12
|
export const toolsDir = path.join(arisaHomeDir, "tools");
|
|
15
13
|
export const tmpDir = path.join(arisaHomeDir, "tmp");
|
|
16
14
|
|
|
17
|
-
export function getChatDir(chatId) {
|
|
18
|
-
return path.join(chatsDir, String(chatId));
|
|
19
|
-
}
|
|
20
|
-
|
|
21
15
|
export function getToolDir(toolName) {
|
|
22
16
|
return path.join(toolsDir, toolName);
|
|
23
17
|
}
|
|
@@ -27,7 +21,7 @@ export function getToolConfigPath(toolName) {
|
|
|
27
21
|
}
|
|
28
22
|
|
|
29
23
|
export function getToolRuntimeDir(toolName) {
|
|
30
|
-
return
|
|
24
|
+
return getToolDir(toolName);
|
|
31
25
|
}
|
|
32
26
|
|
|
33
27
|
export function getToolOutDir(toolName) {
|
|
@@ -41,8 +35,6 @@ export function getToolTmpDir(toolName) {
|
|
|
41
35
|
export async function ensureArisaHome() {
|
|
42
36
|
await mkdir(stateDir, { recursive: true });
|
|
43
37
|
await mkdir(artifactsDir, { recursive: true });
|
|
44
|
-
await mkdir(piAgentDir, { recursive: true });
|
|
45
|
-
await mkdir(chatsDir, { recursive: true });
|
|
46
38
|
await mkdir(toolsDir, { recursive: true });
|
|
47
39
|
await mkdir(tmpDir, { recursive: true });
|
|
48
40
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { DefaultPackageManager, SettingsManager } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { arisaHomeDir } from "./paths.js";
|
|
3
|
+
|
|
4
|
+
function createPackageManager() {
|
|
5
|
+
const settingsManager = SettingsManager.create(arisaHomeDir, arisaHomeDir);
|
|
6
|
+
const packageManager = new DefaultPackageManager({
|
|
7
|
+
cwd: arisaHomeDir,
|
|
8
|
+
agentDir: arisaHomeDir,
|
|
9
|
+
settingsManager
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
packageManager.setProgressCallback((event) => {
|
|
13
|
+
if (event.type === "start") {
|
|
14
|
+
process.stdout.write(`${event.message}\n`);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
return packageManager;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function installPiPackage(source) {
|
|
22
|
+
const packageManager = createPackageManager();
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
await packageManager.installAndPersist(source, { local: false });
|
|
26
|
+
console.log(`Installed ${source}`);
|
|
27
|
+
return { ok: true, code: 0 };
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
30
|
+
return { ok: false, code: 1 };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function removePiPackage(source) {
|
|
35
|
+
const packageManager = createPackageManager();
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const removed = await packageManager.removeAndPersist(source, { local: false });
|
|
39
|
+
if (!removed) {
|
|
40
|
+
console.error(`No matching package found for ${source}`);
|
|
41
|
+
return { ok: false, code: 1 };
|
|
42
|
+
}
|
|
43
|
+
console.log(`Removed ${source}`);
|
|
44
|
+
return { ok: true, code: 0 };
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
47
|
+
return { ok: false, code: 1 };
|
|
48
|
+
}
|
|
49
|
+
}
|