arisa 4.0.18 → 4.0.22
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 +18 -2
- package/README.md +2 -26
- package/package.json +1 -1
- package/src/core/tools/ipc-client.js +5 -1
- package/src/index.js +10 -146
- package/src/runtime/arisa-capabilities.js +34 -1
- package/src/runtime/bootstrap.js +3 -165
- package/src/runtime/create-app.js +3 -3
- package/src/runtime/service-manager.js +2 -2
- package/src/transport/telegram/bot.js +6 -21
- package/test/ipc-server.test.js +116 -3
- package/src/runtime/process-ids.js +0 -9
- package/test/process-ids.test.js +0 -14
package/AGENTS.md
CHANGED
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
- Tools are isolated and each one has its own manifest, entrypoint, and config defaults.
|
|
9
9
|
- No tools ship with the core. All installed tools live under `~/.arisa/tools/<toolName>`; the install directory of Arisa (your working directory) contains only the core. Never create or install tools inside the install directory.
|
|
10
10
|
|
|
11
|
+
## Core modification policy
|
|
12
|
+
Modifying the Arisa core is the last resort, never the default. All work must be done in tools.
|
|
13
|
+
- Always solve the request by creating or editing a tool under `~/.arisa/tools/<toolName>`.
|
|
14
|
+
- Treat changing core code as the final option, only after confirming the capability genuinely cannot be delivered through the tool architecture.
|
|
15
|
+
- Never modify the core on your own initiative. Always consult the user first, explaining why a core change is unavoidable, and wait for explicit approval before touching core code.
|
|
16
|
+
|
|
11
17
|
## Runtime directory rules
|
|
12
18
|
Do not build runtime paths by hand. Use `src/runtime/paths.js`:
|
|
13
19
|
- `getToolDir(toolName)`: installed user tool package only; no runtime data here.
|
|
@@ -33,7 +39,7 @@ Each tool declares in `tool.manifest.json`:
|
|
|
33
39
|
- `skillHints`: optional skills to apply when using or editing the tool
|
|
34
40
|
|
|
35
41
|
## Tool-to-Arisa IPC
|
|
36
|
-
Arisa does not mount tool-provided web routes in core. If a tool needs to expose a web UI or HTTP endpoint, the tool owns that server, usually through the shared daemon runtime. The tool's server handles its own requests and uses Arisa IPC when it needs artifacts, tasks, agent events, or runtime paths.
|
|
42
|
+
Arisa does not mount tool-provided web routes in core. If a tool needs to expose a web UI or HTTP endpoint, the tool owns that server, usually through the shared daemon runtime. The tool's server handles its own requests and uses Arisa IPC when it needs to run a registered tool, create/read artifacts, manage tasks, enqueue agent events, or resolve runtime paths.
|
|
37
43
|
|
|
38
44
|
Installed tools can import the IPC client through `ARISA_PACKAGE_DIR`:
|
|
39
45
|
|
|
@@ -48,7 +54,17 @@ const arisa = createArisaClient({ toolName: "example-tool", chatId });
|
|
|
48
54
|
await arisa.artifacts.createText({ text: "hello" });
|
|
49
55
|
```
|
|
50
56
|
|
|
51
|
-
|
|
57
|
+
For tool-owned web UI request/response flows, call another registered tool through IPC instead of adding core HTTP routes or proxies:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
const result = await arisa.tools.run({
|
|
61
|
+
name: "strudel-agent",
|
|
62
|
+
text: prompt,
|
|
63
|
+
args: { bpm, tags, currentCode }
|
|
64
|
+
}, { timeoutMs: 120_000 });
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The IPC channel is a local socket under `~/.arisa/state`. Every request must include `toolName`; chat-scoped capabilities also require `chatId`. Exposed capabilities are explicit: tools (`run`), artifacts (`createText`, `listRecent`, `get`), tasks (`add`, `list`, `cancel`), agent events (`enqueueEvent`), and runtime paths (`getChatToolStateDir`, `getToolStateDir`, `getChatToolTmpDir`, `getToolTmpDir`, `getChatArtifactsDir`). Do not add raw access to `agentManager`, `taskStore`, `artifactStore`, or `toolRegistry`.
|
|
52
68
|
|
|
53
69
|
## Conceptual pipe model
|
|
54
70
|
There are two different moments where pipes can happen:
|
package/README.md
CHANGED
|
@@ -114,6 +114,7 @@ arisa start # start in background
|
|
|
114
114
|
arisa stop # stop background service
|
|
115
115
|
arisa status # show background service status
|
|
116
116
|
arisa flush # remove ~/.arisa
|
|
117
|
+
arisa --silent # run without verbose logs
|
|
117
118
|
```
|
|
118
119
|
|
|
119
120
|
Runtime model override (current process only):
|
|
@@ -138,32 +139,7 @@ On first run, Arisa will:
|
|
|
138
139
|
6. validate that Pi Agent works
|
|
139
140
|
7. only then start listening to Telegram
|
|
140
141
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
You can skip the interactive questions by providing `--telegram.token` and optional overrides:
|
|
144
|
-
|
|
145
|
-
```bash
|
|
146
|
-
node src/index.js --telegram.token <token>
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
With this mode, Arisa creates `~/.arisa/state/config.json` without prompts and applies these defaults when not provided:
|
|
150
|
-
|
|
151
|
-
- `pi.provider`: `openai-codex` when available, otherwise first provider from the current Pi provider list
|
|
152
|
-
- `pi.model`: first model after bootstrap sorting (currently prioritizes `openai-codex/gpt-5.5`)
|
|
153
|
-
- `telegram.maxChatIds`: `1`
|
|
154
|
-
|
|
155
|
-
Supported overrides:
|
|
156
|
-
|
|
157
|
-
```bash
|
|
158
|
-
node src/index.js --telegram.token <token> --telegram.maxChatIds 3 --pi.provider openai-codex --pi.model gpt-5.5 --pi.apiKey <optional-provider-key>
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
Notes:
|
|
162
|
-
|
|
163
|
-
- interactive bootstrap remains unchanged when no CLI overrides are provided
|
|
164
|
-
- `--bootstrap` can be combined with overrides to regenerate config non-interactively
|
|
165
|
-
- when `--pi.apiKey` is omitted and the provider supports OAuth, bootstrap can use a temporary auth relay if `PORT` is set; this relay is only for Pi login and is not a tool web route server
|
|
166
|
-
- unknown `--pi.provider` or `--pi.model` values are ignored and replaced by safe defaults
|
|
142
|
+
Arisa does not run a persistent HTTP health server or Telegram webhook; Telegram uses long polling.
|
|
167
143
|
|
|
168
144
|
Telegram bot tokens can be created with:
|
|
169
145
|
|
package/package.json
CHANGED
|
@@ -58,8 +58,9 @@ export function createArisaClient({ toolName, chatId = null, socketPath = proces
|
|
|
58
58
|
throw new Error("toolName is required");
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
const call = (method, params = {}) => requestIpc({
|
|
61
|
+
const call = (method, params = {}, options = {}) => requestIpc({
|
|
62
62
|
socketPath,
|
|
63
|
+
timeoutMs: options.timeoutMs,
|
|
63
64
|
request: {
|
|
64
65
|
id: crypto.randomUUID(),
|
|
65
66
|
method,
|
|
@@ -83,6 +84,9 @@ export function createArisaClient({ toolName, chatId = null, socketPath = proces
|
|
|
83
84
|
agent: {
|
|
84
85
|
enqueueEvent: (params) => call("agent.enqueueEvent", params)
|
|
85
86
|
},
|
|
87
|
+
tools: {
|
|
88
|
+
run: (params, options) => call("tools.run", params, options)
|
|
89
|
+
},
|
|
86
90
|
paths: {
|
|
87
91
|
getChatToolStateDir: () => call("paths.getChatToolStateDir"),
|
|
88
92
|
getToolStateDir: () => call("paths.getToolStateDir"),
|
package/src/index.js
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { createServer } from "node:http";
|
|
4
|
-
import { execFile } from "node:child_process";
|
|
5
3
|
import { bootstrapIfNeeded } from "./runtime/bootstrap.js";
|
|
6
4
|
import { createApp } from "./runtime/create-app.js";
|
|
7
5
|
import { createLogger } from "./runtime/logger.js";
|
|
8
6
|
import { getServiceStatus, registerServiceProcess, startService, stopService, unregisterServiceProcess } from "./runtime/service-manager.js";
|
|
9
7
|
import { flushArisaHome } from "./runtime/flush.js";
|
|
10
8
|
import { arisaPackageDir } from "./runtime/paths.js";
|
|
11
|
-
import { parseProcessIds } from "./runtime/process-ids.js";
|
|
12
9
|
|
|
13
10
|
process.env.ARISA_PACKAGE_DIR = arisaPackageDir;
|
|
14
11
|
|
|
@@ -16,136 +13,13 @@ const args = process.argv.slice(2);
|
|
|
16
13
|
const cli = parseCliArgs(args);
|
|
17
14
|
const command = cli.positionals[0] || "run";
|
|
18
15
|
const forceBootstrap = Boolean(cli.flags.bootstrap);
|
|
19
|
-
const verbose =
|
|
16
|
+
const verbose = !cli.flags.silent;
|
|
20
17
|
const serviceRunner = Boolean(cli.flags["service-runner"]);
|
|
21
|
-
const
|
|
22
|
-
const runtimeOverrides = toRuntimeOverrides(cli.nestedFlags);
|
|
18
|
+
const runtimeOverrides = toNestedOverrides(cli.nestedFlags);
|
|
23
19
|
const logger = createLogger({ verbose });
|
|
24
20
|
let activeApp = null;
|
|
25
21
|
let shuttingDown = false;
|
|
26
22
|
|
|
27
|
-
const defaultHttpPort = 11970;
|
|
28
|
-
const httpPort = Number(process.env.ARISA_HTTP_PORT || defaultHttpPort);
|
|
29
|
-
const shouldStartHttpServer = Boolean(httpPort && !["stop", "status", "flush"].includes(command));
|
|
30
|
-
let httpRequestHandler = null;
|
|
31
|
-
let httpServerListening = false;
|
|
32
|
-
let httpServer = null;
|
|
33
|
-
|
|
34
|
-
function setHttpRequestHandler(handler) {
|
|
35
|
-
httpRequestHandler = handler;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const httpServerReady = shouldStartHttpServer
|
|
39
|
-
? startHttpServer()
|
|
40
|
-
: Promise.resolve(false);
|
|
41
|
-
|
|
42
|
-
async function runCommand(commandName, args = []) {
|
|
43
|
-
return new Promise((resolve) => {
|
|
44
|
-
execFile(commandName, args, (error, stdout = "") => {
|
|
45
|
-
resolve(error ? "" : stdout.trim());
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function getListeningPids(port) {
|
|
51
|
-
const lsofOutput = await runCommand("lsof", ["-tiTCP", `-sTCP:LISTEN`, `-iTCP:${port}`]);
|
|
52
|
-
if (lsofOutput) {
|
|
53
|
-
return parseProcessIds(lsofOutput);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const fuserOutput = await runCommand("fuser", ["-n", "tcp", String(port)]);
|
|
57
|
-
return parseProcessIds(fuserOutput);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function getProcessCommand(pid) {
|
|
61
|
-
return runCommand("ps", ["-p", String(pid), "-o", "command="]);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function looksLikeArisaProcess(commandText) {
|
|
65
|
-
return /\barisa\b/.test(commandText) || /\/arisa\/src\/index\.js\b/i.test(commandText);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function isProcessRunning(pid) {
|
|
69
|
-
try {
|
|
70
|
-
process.kill(pid, 0);
|
|
71
|
-
return true;
|
|
72
|
-
} catch {
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async function waitForProcessExit(pid, timeoutMs = 3000) {
|
|
78
|
-
const deadline = Date.now() + timeoutMs;
|
|
79
|
-
while (Date.now() < deadline) {
|
|
80
|
-
if (!isProcessRunning(pid)) return true;
|
|
81
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
82
|
-
}
|
|
83
|
-
return !isProcessRunning(pid);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async function stopStaleArisaListeners(port) {
|
|
87
|
-
const pids = await getListeningPids(port);
|
|
88
|
-
for (const pid of pids) {
|
|
89
|
-
if (pid === process.pid) continue;
|
|
90
|
-
const commandText = await getProcessCommand(pid);
|
|
91
|
-
if (!looksLikeArisaProcess(commandText)) {
|
|
92
|
-
logger.error("http", `port ${port} is already used by pid ${pid}; not killing non-Arisa process`);
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
logger.log("http", `stopping stale Arisa listener on port ${port} (pid ${pid})`);
|
|
97
|
-
try {
|
|
98
|
-
process.kill(pid, "SIGTERM");
|
|
99
|
-
if (!await waitForProcessExit(pid)) {
|
|
100
|
-
process.kill(pid, "SIGKILL");
|
|
101
|
-
await waitForProcessExit(pid, 1000);
|
|
102
|
-
}
|
|
103
|
-
} catch (error) {
|
|
104
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
105
|
-
logger.error("http", `failed to stop stale Arisa listener pid ${pid}: ${message}`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async function startHttpServer() {
|
|
111
|
-
await stopStaleArisaListeners(httpPort);
|
|
112
|
-
return new Promise((resolve) => {
|
|
113
|
-
const server = createServer((req, res) => {
|
|
114
|
-
if (httpRequestHandler) return httpRequestHandler(req, res);
|
|
115
|
-
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
116
|
-
res.end("ok");
|
|
117
|
-
return undefined;
|
|
118
|
-
});
|
|
119
|
-
httpServer = server;
|
|
120
|
-
server.on("error", (error) => {
|
|
121
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
122
|
-
logger.error("http", `server failed on port ${httpPort}: ${message}`);
|
|
123
|
-
resolve(false);
|
|
124
|
-
});
|
|
125
|
-
server.listen(httpPort)
|
|
126
|
-
.on("listening", () => {
|
|
127
|
-
httpServerListening = true;
|
|
128
|
-
logger.log("http", `health server on port ${httpPort}`);
|
|
129
|
-
resolve(true);
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async function getHttpOptions() {
|
|
135
|
-
await httpServerReady;
|
|
136
|
-
return httpServerListening ? { httpPort, setHttpRequestHandler } : {};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
async function stopHttpServer() {
|
|
140
|
-
if (!httpServerListening || !httpServer) return;
|
|
141
|
-
await new Promise((resolve) => {
|
|
142
|
-
httpServer.close(() => resolve());
|
|
143
|
-
});
|
|
144
|
-
httpServer = null;
|
|
145
|
-
httpServerListening = false;
|
|
146
|
-
httpRequestHandler = null;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
23
|
function parseCliArgs(rawArgs) {
|
|
150
24
|
const flags = {};
|
|
151
25
|
const nestedFlags = {};
|
|
@@ -176,7 +50,7 @@ function parseCliArgs(rawArgs) {
|
|
|
176
50
|
return { flags, nestedFlags, positionals };
|
|
177
51
|
}
|
|
178
52
|
|
|
179
|
-
function
|
|
53
|
+
function toNestedOverrides(nestedFlags) {
|
|
180
54
|
const overrides = {};
|
|
181
55
|
for (const [flatKey, value] of Object.entries(nestedFlags)) {
|
|
182
56
|
const parts = flatKey.split(".");
|
|
@@ -193,10 +67,6 @@ function toBootstrapOverrides(nestedFlags) {
|
|
|
193
67
|
return overrides;
|
|
194
68
|
}
|
|
195
69
|
|
|
196
|
-
function toRuntimeOverrides(nestedFlags) {
|
|
197
|
-
return toBootstrapOverrides(nestedFlags);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
70
|
function toServiceRunnerArgs(nestedFlags) {
|
|
201
71
|
const args = [];
|
|
202
72
|
if (nestedFlags["pi.model"]) {
|
|
@@ -205,14 +75,11 @@ function toServiceRunnerArgs(nestedFlags) {
|
|
|
205
75
|
return args;
|
|
206
76
|
}
|
|
207
77
|
|
|
208
|
-
const webhookUrl = bootstrapOverrides.webhook?.url || "";
|
|
209
|
-
|
|
210
78
|
async function shutdown(exitCode = 0) {
|
|
211
79
|
if (shuttingDown) return;
|
|
212
80
|
shuttingDown = true;
|
|
213
81
|
try {
|
|
214
82
|
await activeApp?.stop?.();
|
|
215
|
-
await stopHttpServer();
|
|
216
83
|
} catch (error) {
|
|
217
84
|
logger.error("app", `shutdown failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
218
85
|
exitCode = exitCode || 1;
|
|
@@ -231,8 +98,8 @@ process.once("SIGINT", () => {
|
|
|
231
98
|
shutdown(0);
|
|
232
99
|
});
|
|
233
100
|
|
|
234
|
-
async function startRuntimeApp(
|
|
235
|
-
const app = await createApp({ logger, runtimeOverrides
|
|
101
|
+
async function startRuntimeApp() {
|
|
102
|
+
const app = await createApp({ logger, runtimeOverrides });
|
|
236
103
|
activeApp = app;
|
|
237
104
|
await app.start();
|
|
238
105
|
}
|
|
@@ -244,10 +111,9 @@ async function runForeground() {
|
|
|
244
111
|
|| runtimeOverrides?.pi?.apiKey
|
|
245
112
|
);
|
|
246
113
|
logger.log("app", `starting${verbose ? " in verbose mode" : ""}`);
|
|
247
|
-
|
|
248
|
-
await bootstrapIfNeeded({ force: forceBootstrap, cliConfigOverrides: bootstrapOverrides, ...httpOptions });
|
|
114
|
+
await bootstrapIfNeeded({ force: forceBootstrap });
|
|
249
115
|
try {
|
|
250
|
-
await startRuntimeApp(
|
|
116
|
+
await startRuntimeApp();
|
|
251
117
|
} catch (error) {
|
|
252
118
|
const message = error instanceof Error ? error.message : String(error);
|
|
253
119
|
if (message.includes("No auth found")) {
|
|
@@ -260,8 +126,8 @@ async function runForeground() {
|
|
|
260
126
|
throw error;
|
|
261
127
|
}
|
|
262
128
|
console.log("Reopening bootstrap so you can provide a Pi API key or switch to a provider you already authenticated with.\n");
|
|
263
|
-
await bootstrapIfNeeded({ force: true
|
|
264
|
-
await startRuntimeApp(
|
|
129
|
+
await bootstrapIfNeeded({ force: true });
|
|
130
|
+
await startRuntimeApp();
|
|
265
131
|
return;
|
|
266
132
|
}
|
|
267
133
|
throw error;
|
|
@@ -276,9 +142,7 @@ async function main() {
|
|
|
276
142
|
}
|
|
277
143
|
|
|
278
144
|
if (command === "start") {
|
|
279
|
-
|
|
280
|
-
await bootstrapIfNeeded({ force: forceBootstrap, cliConfigOverrides: bootstrapOverrides, ...httpOptions });
|
|
281
|
-
await stopHttpServer();
|
|
145
|
+
await bootstrapIfNeeded({ force: forceBootstrap });
|
|
282
146
|
const result = await startService({ verbose, cliArgs: toServiceRunnerArgs(cli.nestedFlags) });
|
|
283
147
|
if (!result.ok) {
|
|
284
148
|
console.log(`Arisa is already running in background (pid ${result.pid}).`);
|
|
@@ -27,16 +27,49 @@ function requireString(value, fieldName) {
|
|
|
27
27
|
return value;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
function normalizeArgs(args) {
|
|
31
|
+
if (args == null) return {};
|
|
32
|
+
if (typeof args !== "object" || Array.isArray(args)) {
|
|
33
|
+
throw new Error("args must be an object");
|
|
34
|
+
}
|
|
35
|
+
return args;
|
|
36
|
+
}
|
|
37
|
+
|
|
30
38
|
function normalizeLimit(limit) {
|
|
31
39
|
const value = Number(limit);
|
|
32
40
|
if (!Number.isInteger(value) || value <= 0) return 20;
|
|
33
41
|
return Math.min(value, 100);
|
|
34
42
|
}
|
|
35
43
|
|
|
36
|
-
export function createArisaCapabilities({ artifactStore, taskStore } = {}) {
|
|
44
|
+
export function createArisaCapabilities({ artifactStore, taskStore, agentManager } = {}) {
|
|
37
45
|
async function dispatch({ method, toolName, chatId = null, params = {} } = {}) {
|
|
38
46
|
const scopedToolName = requireToolName(toolName);
|
|
39
47
|
|
|
48
|
+
if (method === "tools.run") {
|
|
49
|
+
if (!agentManager?.runTool) {
|
|
50
|
+
throw new Error("tools.run requires agentManager");
|
|
51
|
+
}
|
|
52
|
+
const scopedChatId = requireChatId(chatId, method);
|
|
53
|
+
const targetToolName = requireString(params.name, "name");
|
|
54
|
+
const chatArtifactStore = artifactStore.forChat(scopedChatId);
|
|
55
|
+
const artifact = params.artifactId
|
|
56
|
+
? await chatArtifactStore.get(requireString(params.artifactId, "artifactId"))
|
|
57
|
+
: null;
|
|
58
|
+
if (params.artifactId && !artifact) {
|
|
59
|
+
throw new Error(`Artifact not found: ${params.artifactId}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return agentManager.runTool({
|
|
63
|
+
name: targetToolName,
|
|
64
|
+
request: {
|
|
65
|
+
artifact,
|
|
66
|
+
text: params.text,
|
|
67
|
+
args: normalizeArgs(params.args)
|
|
68
|
+
},
|
|
69
|
+
chatId: scopedChatId
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
40
73
|
if (method === "artifacts.createText") {
|
|
41
74
|
const scopedChatId = requireChatId(chatId, method);
|
|
42
75
|
return artifactStore.forChat(scopedChatId).createText({
|
package/src/runtime/bootstrap.js
CHANGED
|
@@ -24,17 +24,6 @@ async function exists(file) {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
function normalizeString(value) {
|
|
28
|
-
if (typeof value !== "string") return "";
|
|
29
|
-
return value.trim();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function parseMaxChatIds(value, fallback = 1) {
|
|
33
|
-
const parsed = Number(value);
|
|
34
|
-
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
|
35
|
-
return parsed;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
27
|
function buildConfig({ telegramApiKey, telegramMaxChatIds, provider, model, piApiKey }) {
|
|
39
28
|
return {
|
|
40
29
|
telegram: {
|
|
@@ -52,37 +41,6 @@ function buildConfig({ telegramApiKey, telegramMaxChatIds, provider, model, piAp
|
|
|
52
41
|
};
|
|
53
42
|
}
|
|
54
43
|
|
|
55
|
-
function resolvePiDefaults(runtime, { provider: preferredProvider = "", model: preferredModel = "" } = {}) {
|
|
56
|
-
const providers = sortBootstrapProviders(listPiProviders(runtime));
|
|
57
|
-
if (!providers.length) {
|
|
58
|
-
throw new Error("No Pi providers are available for bootstrap.");
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const preferredProviderValue = normalizeString(preferredProvider);
|
|
62
|
-
const providerExists = providers.some((item) => item.provider === preferredProviderValue);
|
|
63
|
-
if (preferredProviderValue && !providerExists) {
|
|
64
|
-
console.log(`Ignoring unknown Pi provider override: ${preferredProviderValue}`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const selectedProvider = providerExists
|
|
68
|
-
? preferredProviderValue
|
|
69
|
-
: providers[0].provider;
|
|
70
|
-
|
|
71
|
-
const models = sortBootstrapModels(selectedProvider, listProviderModels(selectedProvider, runtime));
|
|
72
|
-
if (!models.length) {
|
|
73
|
-
throw new Error(`No Pi models are available for provider ${selectedProvider}.`);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const preferredModelValue = normalizeString(preferredModel);
|
|
77
|
-
const modelExists = models.some((item) => item.id === preferredModelValue);
|
|
78
|
-
if (preferredModelValue && !modelExists) {
|
|
79
|
-
console.log(`Ignoring unknown Pi model override for ${selectedProvider}: ${preferredModelValue}`);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const selectedModel = modelExists ? preferredModelValue : models[0].id;
|
|
83
|
-
return { provider: selectedProvider, model: selectedModel };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
44
|
function sortBootstrapProviders(providers) {
|
|
87
45
|
const preferredOrder = ["openai-codex"];
|
|
88
46
|
const positions = new Map(providers.map((provider, index) => [provider.provider, index]));
|
|
@@ -131,89 +89,13 @@ async function maybeOpenExternal(url) {
|
|
|
131
89
|
});
|
|
132
90
|
}
|
|
133
91
|
|
|
134
|
-
function
|
|
135
|
-
let authUrl = "";
|
|
136
|
-
let resolveRedirectUrl;
|
|
137
|
-
const redirectUrlPromise = new Promise((resolve) => {
|
|
138
|
-
resolveRedirectUrl = resolve;
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
const page = (body) => [
|
|
142
|
-
"<!DOCTYPE html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width'>",
|
|
143
|
-
"<title>Arisa Auth</title>",
|
|
144
|
-
"<style>body{font-family:system-ui,sans-serif;max-width:600px;margin:40px auto;padding:0 20px;line-height:1.6}",
|
|
145
|
-
"input[type=text]{width:100%;padding:8px;box-sizing:border-box;margin:8px 0}",
|
|
146
|
-
"button{padding:8px 24px;cursor:pointer}code{background:#f0f0f0;padding:2px 6px;border-radius:3px}</style>",
|
|
147
|
-
"</head><body>",
|
|
148
|
-
body,
|
|
149
|
-
"</body></html>"
|
|
150
|
-
].join("");
|
|
151
|
-
|
|
152
|
-
setHttpRequestHandler((req, res) => {
|
|
153
|
-
const parsed = new URL(req.url, `http://localhost:${httpPort}`);
|
|
154
|
-
|
|
155
|
-
if (req.method === "GET" && parsed.pathname === "/auth/callback" && parsed.searchParams.has("code")) {
|
|
156
|
-
const callbackUrl = `http://localhost:1455${parsed.pathname}${parsed.search}`;
|
|
157
|
-
resolveRedirectUrl(callbackUrl);
|
|
158
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
159
|
-
res.end(page("<h2>Authentication received</h2><p>You can close this page. Arisa is starting…</p>"));
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (req.method === "GET" && parsed.pathname === "/") {
|
|
164
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
165
|
-
res.end(page([
|
|
166
|
-
"<h2>Arisa — Pi Authentication</h2>",
|
|
167
|
-
authUrl
|
|
168
|
-
? `<p><strong>1.</strong> <a href="${authUrl}" target="_blank">Click here to log in with Pi</a></p>`
|
|
169
|
-
: "<p>Waiting for authentication URL…</p>",
|
|
170
|
-
"<p><strong>2.</strong> After login your browser will redirect to a <code>localhost</code> URL that won't load. That's expected.</p>",
|
|
171
|
-
"<p><strong>3.</strong> In your browser's address bar, replace <code>localhost:1455</code> with your server's domain and press Enter.</p>",
|
|
172
|
-
"<hr>",
|
|
173
|
-
"<p><em>Or paste the full redirect URL here:</em></p>",
|
|
174
|
-
'<form method="POST" action="/auth/relay">',
|
|
175
|
-
'<input type="text" name="url" placeholder="Paste the localhost redirect URL here…" required />',
|
|
176
|
-
"<button type='submit'>Submit</button>",
|
|
177
|
-
"</form>"
|
|
178
|
-
].join("\n")));
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (req.method === "POST" && parsed.pathname === "/auth/relay") {
|
|
183
|
-
let body = "";
|
|
184
|
-
req.on("data", (chunk) => { body += chunk; });
|
|
185
|
-
req.on("end", () => {
|
|
186
|
-
const url = (new URLSearchParams(body).get("url") || "").trim();
|
|
187
|
-
if (url) resolveRedirectUrl(url);
|
|
188
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
189
|
-
res.end(page("<h2>Authentication received</h2><p>You can close this page. Arisa is starting…</p>"));
|
|
190
|
-
});
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
195
|
-
res.end("ok");
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
return {
|
|
199
|
-
setAuthUrl(url) { authUrl = url; },
|
|
200
|
-
waitForRedirectUrl() { return redirectUrlPromise; },
|
|
201
|
-
uninstall() { setHttpRequestHandler(null); }
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
async function runInternalPiLogin(provider, { rl = null, authRelay = null } = {}) {
|
|
92
|
+
async function runInternalPiLogin(provider, { rl = null } = {}) {
|
|
206
93
|
const login = createPiOAuthLogin({
|
|
207
94
|
provider,
|
|
208
95
|
onAuth: async ({ url, instructions, controller }) => {
|
|
209
96
|
console.log(`${instructions || "Open this URL to continue authentication:"}\n${url}\n`);
|
|
210
97
|
await maybeOpenExternal(url);
|
|
211
|
-
if (
|
|
212
|
-
authRelay.setAuthUrl(url);
|
|
213
|
-
console.log("Waiting for authentication via the web relay...");
|
|
214
|
-
const redirectUrl = await authRelay.waitForRedirectUrl();
|
|
215
|
-
if (redirectUrl) controller.submitManualCode(redirectUrl);
|
|
216
|
-
} else if (controller.oauthProvider.usesCallbackServer && rl) {
|
|
98
|
+
if (controller.oauthProvider.usesCallbackServer && rl) {
|
|
217
99
|
const pasted = (await rl.question("Paste the redirect URL here if the browser does not return automatically, or press Enter to keep waiting: ")).trim();
|
|
218
100
|
if (pasted) controller.submitManualCode(pasted);
|
|
219
101
|
}
|
|
@@ -236,54 +118,10 @@ async function runInternalPiLogin(provider, { rl = null, authRelay = null } = {}
|
|
|
236
118
|
await login.promise;
|
|
237
119
|
}
|
|
238
120
|
|
|
239
|
-
export async function bootstrapIfNeeded({ force = false
|
|
121
|
+
export async function bootstrapIfNeeded({ force = false } = {}) {
|
|
240
122
|
await ensureArisaHome();
|
|
241
123
|
if (!force && await exists(configFile)) return;
|
|
242
124
|
|
|
243
|
-
const telegramApiKeyFromCli = normalizeString(cliConfigOverrides?.telegram?.token);
|
|
244
|
-
if (telegramApiKeyFromCli) {
|
|
245
|
-
const runtime = createPiRuntime();
|
|
246
|
-
const resolvedPi = resolvePiDefaults(runtime, cliConfigOverrides?.pi || {});
|
|
247
|
-
const telegramMaxChatIds = parseMaxChatIds(cliConfigOverrides?.telegram?.maxChatIds, 1);
|
|
248
|
-
const piApiKey = normalizeString(cliConfigOverrides?.pi?.apiKey);
|
|
249
|
-
if (!piApiKey && !hasProviderAuth(resolvedPi.provider, runtime)) {
|
|
250
|
-
if (!supportsProviderOAuth(resolvedPi.provider, runtime)) {
|
|
251
|
-
throw new Error(
|
|
252
|
-
`No auth found for ${resolvedPi.provider}. Provide --pi.apiKey for non-interactive bootstrap, or use a provider that supports OAuth.`
|
|
253
|
-
);
|
|
254
|
-
}
|
|
255
|
-
if (!httpPort || !setHttpRequestHandler) {
|
|
256
|
-
throw new Error(
|
|
257
|
-
`No auth found for ${resolvedPi.provider}. Arisa's HTTP server is unavailable; set ARISA_HTTP_PORT or free the configured port.`
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
const authRelay = installAuthRelay(httpPort, setHttpRequestHandler);
|
|
261
|
-
console.log(`No existing Pi auth found for ${resolvedPi.provider}. Auth relay active on port ${httpPort}.`);
|
|
262
|
-
console.log(`Open your server URL in a browser to complete Pi authentication.\n`);
|
|
263
|
-
try {
|
|
264
|
-
await runInternalPiLogin(resolvedPi.provider, { authRelay });
|
|
265
|
-
} finally {
|
|
266
|
-
authRelay.uninstall();
|
|
267
|
-
}
|
|
268
|
-
if (!hasProviderAuth(resolvedPi.provider, createPiRuntime())) {
|
|
269
|
-
throw new Error(
|
|
270
|
-
`Pi login did not complete for ${resolvedPi.provider}. Retry or provide --pi.apiKey.`
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
console.log(`Detected Pi auth for ${resolvedPi.provider}. Continuing bootstrap.`);
|
|
274
|
-
}
|
|
275
|
-
const config = buildConfig({
|
|
276
|
-
telegramApiKey: telegramApiKeyFromCli,
|
|
277
|
-
telegramMaxChatIds,
|
|
278
|
-
provider: resolvedPi.provider,
|
|
279
|
-
model: resolvedPi.model,
|
|
280
|
-
piApiKey
|
|
281
|
-
});
|
|
282
|
-
await writeFile(configFile, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
283
|
-
console.log(`\nConfig saved to ${configFile} (non-interactive bootstrap)\n`);
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
125
|
const rl = readline.createInterface({ input, output });
|
|
288
126
|
const ask = async (label, fallback = "") => {
|
|
289
127
|
const suffix = fallback ? ` (${fallback})` : "";
|
|
@@ -46,7 +46,7 @@ function applyRuntimeOverrides(config, runtimeOverrides) {
|
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
export async function createApp({ logger, runtimeOverrides
|
|
49
|
+
export async function createApp({ logger, runtimeOverrides } = {}) {
|
|
50
50
|
logger?.log("app", "loading config");
|
|
51
51
|
const persistedConfig = await loadConfig();
|
|
52
52
|
const config = applyRuntimeOverrides(persistedConfig, runtimeOverrides);
|
|
@@ -62,9 +62,9 @@ export async function createApp({ logger, runtimeOverrides, webhookUrl, setHttpR
|
|
|
62
62
|
logger?.log("app", `loaded ${toolRegistry.list().length} tools`);
|
|
63
63
|
|
|
64
64
|
const agentManager = new AgentManager({ config, artifactStore, toolRegistry, taskStore, logger });
|
|
65
|
-
const arisaCapabilities = createArisaCapabilities({ artifactStore, taskStore });
|
|
65
|
+
const arisaCapabilities = createArisaCapabilities({ artifactStore, taskStore, agentManager });
|
|
66
66
|
const ipcServer = createIpcServer({ capabilities: arisaCapabilities, logger });
|
|
67
|
-
const bot = await createTelegramBot({ config, artifactStore, toolRegistry, taskStore, agentManager, saveConfig, updateConfig, logger
|
|
67
|
+
const bot = await createTelegramBot({ config, artifactStore, toolRegistry, taskStore, agentManager, saveConfig, updateConfig, logger });
|
|
68
68
|
|
|
69
69
|
return {
|
|
70
70
|
async start() {
|
|
@@ -36,7 +36,7 @@ export async function getServiceStatus() {
|
|
|
36
36
|
return { running: true, pid };
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export async function startService({ verbose =
|
|
39
|
+
export async function startService({ verbose = true, cliArgs = [] } = {}) {
|
|
40
40
|
await ensureArisaHome();
|
|
41
41
|
const status = await getServiceStatus();
|
|
42
42
|
if (status.running) {
|
|
@@ -45,7 +45,7 @@ export async function startService({ verbose = false, cliArgs = [] } = {}) {
|
|
|
45
45
|
|
|
46
46
|
const logHandle = await open(serviceLogFile, "a");
|
|
47
47
|
const args = [entryFile, "--service-runner", ...cliArgs];
|
|
48
|
-
if (verbose) args.push("--
|
|
48
|
+
if (!verbose) args.push("--silent");
|
|
49
49
|
|
|
50
50
|
const child = spawn(process.execPath, args, {
|
|
51
51
|
detached: true,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Bot, InputFile
|
|
1
|
+
import { Bot, InputFile } from "grammy";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { authorizeChat } from "./auth.js";
|
|
4
4
|
import { captureIncomingArtifact } from "./media.js";
|
|
@@ -251,7 +251,7 @@ async function withTyping(ctx, work) {
|
|
|
251
251
|
}
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
export async function createTelegramBot({ config, artifactStore, toolRegistry, taskStore, agentManager, saveConfig, updateConfig, logger
|
|
254
|
+
export async function createTelegramBot({ config, artifactStore, toolRegistry, taskStore, agentManager, saveConfig, updateConfig, logger }) {
|
|
255
255
|
const bot = new Bot(config.telegram.token);
|
|
256
256
|
const perChatState = new Map();
|
|
257
257
|
const notifiedPromptErrors = new WeakSet();
|
|
@@ -736,25 +736,10 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
736
736
|
}, 1000);
|
|
737
737
|
taskTimer.unref();
|
|
738
738
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
onTimeout: "return",
|
|
744
|
-
});
|
|
745
|
-
webRouter.registerCoreRoute({
|
|
746
|
-
method: "POST",
|
|
747
|
-
path: webhookPath,
|
|
748
|
-
handler: handleUpdate
|
|
749
|
-
});
|
|
750
|
-
await bot.api.setWebhook(`${webhookUrl}${webhookPath}`);
|
|
751
|
-
logger?.log("telegram", `webhook mode: ${webhookUrl}${webhookPath}`);
|
|
752
|
-
scheduleStartupMessages({ skipAgentStartupPrompts });
|
|
753
|
-
} else {
|
|
754
|
-
logger?.log("telegram", "bot polling started");
|
|
755
|
-
scheduleStartupMessages({ skipAgentStartupPrompts });
|
|
756
|
-
await bot.start({ drop_pending_updates: true });
|
|
757
|
-
}
|
|
739
|
+
await bot.api.deleteWebhook({ drop_pending_updates: true });
|
|
740
|
+
logger?.log("telegram", "bot polling started");
|
|
741
|
+
scheduleStartupMessages({ skipAgentStartupPrompts });
|
|
742
|
+
await bot.start();
|
|
758
743
|
},
|
|
759
744
|
|
|
760
745
|
async stop() {
|
package/test/ipc-server.test.js
CHANGED
|
@@ -59,13 +59,18 @@ function createFakeTaskStore() {
|
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
function createCapabilities() {
|
|
62
|
+
function createCapabilities(overrides = {}) {
|
|
63
63
|
return createArisaCapabilities({
|
|
64
|
-
artifactStore: createFakeArtifactStore(),
|
|
65
|
-
taskStore: createFakeTaskStore()
|
|
64
|
+
artifactStore: overrides.artifactStore || createFakeArtifactStore(),
|
|
65
|
+
taskStore: overrides.taskStore || createFakeTaskStore(),
|
|
66
|
+
agentManager: overrides.agentManager
|
|
66
67
|
});
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
function wait(ms) {
|
|
71
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
72
|
+
}
|
|
73
|
+
|
|
69
74
|
async function createTempSocketPath() {
|
|
70
75
|
const root = await mkdtemp(path.join(os.tmpdir(), "arisa-ipc-"));
|
|
71
76
|
return path.join(root, "arisa.sock");
|
|
@@ -141,6 +146,114 @@ test("requires chatId for chat-scoped capabilities", async () => {
|
|
|
141
146
|
}
|
|
142
147
|
});
|
|
143
148
|
|
|
149
|
+
test("runs a registered tool over local IPC", async () => {
|
|
150
|
+
const socketPath = await createTempSocketPath();
|
|
151
|
+
const calls = [];
|
|
152
|
+
const agentManager = {
|
|
153
|
+
runTool: async (request) => {
|
|
154
|
+
calls.push(request);
|
|
155
|
+
return { ok: true, status: "ok", output: { text: "generated code" } };
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
const ipcServer = createIpcServer({ capabilities: createCapabilities({ agentManager }), socketPath });
|
|
159
|
+
await ipcServer.start();
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const client = createArisaClient({ toolName: "strudel-ui", chatId: "chat-1", socketPath });
|
|
163
|
+
const result = await client.tools.run({
|
|
164
|
+
name: "strudel-agent",
|
|
165
|
+
text: "make acid house",
|
|
166
|
+
args: {
|
|
167
|
+
bpm: 128,
|
|
168
|
+
tags: ["acid", "live"],
|
|
169
|
+
currentCode: "stack(s(\"bd\"))"
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
assert.deepEqual(result, { ok: true, status: "ok", output: { text: "generated code" } });
|
|
174
|
+
assert.equal(calls.length, 1);
|
|
175
|
+
assert.equal(calls[0].name, "strudel-agent");
|
|
176
|
+
assert.equal(calls[0].chatId, "chat-1");
|
|
177
|
+
assert.equal(calls[0].request.text, "make acid house");
|
|
178
|
+
assert.deepEqual(calls[0].request.args, {
|
|
179
|
+
bpm: 128,
|
|
180
|
+
tags: ["acid", "live"],
|
|
181
|
+
currentCode: "stack(s(\"bd\"))"
|
|
182
|
+
});
|
|
183
|
+
assert.equal(calls[0].request.artifact, null);
|
|
184
|
+
} finally {
|
|
185
|
+
await ipcServer.stop();
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("hydrates artifact input before running a tool over IPC", async () => {
|
|
190
|
+
const socketPath = await createTempSocketPath();
|
|
191
|
+
const calls = [];
|
|
192
|
+
const agentManager = {
|
|
193
|
+
runTool: async (request) => {
|
|
194
|
+
calls.push(request);
|
|
195
|
+
return { ok: true, status: "ok", output: { json: { received: true } } };
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
const ipcServer = createIpcServer({ capabilities: createCapabilities({ agentManager }), socketPath });
|
|
199
|
+
await ipcServer.start();
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const client = createArisaClient({ toolName: "strudel-ui", chatId: 123, socketPath });
|
|
203
|
+
const artifact = await client.artifacts.createText({ text: "current pattern" });
|
|
204
|
+
await client.tools.run({ name: "strudel-agent", artifactId: artifact.id });
|
|
205
|
+
|
|
206
|
+
assert.equal(calls.length, 1);
|
|
207
|
+
assert.deepEqual(calls[0].request.artifact, artifact);
|
|
208
|
+
} finally {
|
|
209
|
+
await ipcServer.stop();
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("requires chatId when running a tool over IPC", async () => {
|
|
214
|
+
const socketPath = await createTempSocketPath();
|
|
215
|
+
const agentManager = {
|
|
216
|
+
runTool: async () => ({ ok: true, status: "ok" })
|
|
217
|
+
};
|
|
218
|
+
const ipcServer = createIpcServer({ capabilities: createCapabilities({ agentManager }), socketPath });
|
|
219
|
+
await ipcServer.start();
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const client = createArisaClient({ toolName: "strudel-ui", socketPath });
|
|
223
|
+
await assert.rejects(
|
|
224
|
+
() => client.tools.run({ name: "strudel-agent" }),
|
|
225
|
+
/tools\.run requires chatId/
|
|
226
|
+
);
|
|
227
|
+
} finally {
|
|
228
|
+
await ipcServer.stop();
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("supports per-call IPC timeout overrides", async () => {
|
|
233
|
+
const socketPath = await createTempSocketPath();
|
|
234
|
+
const agentManager = {
|
|
235
|
+
runTool: async () => {
|
|
236
|
+
await wait(25);
|
|
237
|
+
return { ok: true, status: "ok" };
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
const ipcServer = createIpcServer({ capabilities: createCapabilities({ agentManager }), socketPath });
|
|
241
|
+
await ipcServer.start();
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
const client = createArisaClient({ toolName: "strudel-ui", chatId: 123, socketPath });
|
|
245
|
+
const result = await client.tools.run({ name: "strudel-agent" }, { timeoutMs: 250 });
|
|
246
|
+
assert.deepEqual(result, { ok: true, status: "ok" });
|
|
247
|
+
|
|
248
|
+
await assert.rejects(
|
|
249
|
+
() => client.tools.run({ name: "strudel-agent" }, { timeoutMs: 1 }),
|
|
250
|
+
/Arisa IPC request timed out/
|
|
251
|
+
);
|
|
252
|
+
} finally {
|
|
253
|
+
await ipcServer.stop();
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
144
257
|
test("listens on a local socket, not a TCP port", async () => {
|
|
145
258
|
const socketPath = await createTempSocketPath();
|
|
146
259
|
const ipcServer = createIpcServer({ capabilities: createCapabilities(), socketPath });
|
package/test/process-ids.test.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import test from "node:test";
|
|
3
|
-
import { parseProcessIds } from "../src/runtime/process-ids.js";
|
|
4
|
-
|
|
5
|
-
test("ignores empty PID command output", () => {
|
|
6
|
-
assert.deepEqual(parseProcessIds(""), []);
|
|
7
|
-
assert.deepEqual(parseProcessIds(" \n\t"), []);
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
test("parses only positive integer PID tokens", () => {
|
|
11
|
-
assert.deepEqual(parseProcessIds("123\n456"), [123, 456]);
|
|
12
|
-
assert.deepEqual(parseProcessIds("11970/tcp: 1234"), [1234]);
|
|
13
|
-
assert.deepEqual(parseProcessIds("0 nope -12 42"), [42]);
|
|
14
|
-
});
|