arisa 4.0.24 → 4.1.4

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 CHANGED
@@ -132,13 +132,15 @@ Notes:
132
132
  On first run, Arisa will:
133
133
 
134
134
  1. ask for a Telegram bot token
135
- 2. ask for the maximum number of authorized chat ids
136
- 3. show Pi providers discovered from Pi Agent's model registry
137
- 4. show the models available for the selected provider
138
- 5. resolve authentication for the selected Pi provider
135
+ 2. validate the token and ask whether to continue bootstrap from Telegram (default: yes)
136
+ 3. when Telegram setup is selected, show a `https://t.me/<bot>?start=<setup-token>` link
137
+ 4. authorize that Telegram chat and continue setup there: Pi provider, model, and Pi auth
138
+ 5. ask from Telegram whether Arisa should keep running in background
139
139
  6. validate that Pi Agent works
140
140
  7. only then start listening to Telegram
141
141
 
142
+ Choosing `n` at the Telegram setup prompt keeps the previous CLI-only bootstrap flow.
143
+
142
144
  Arisa does not run a persistent HTTP health server or Telegram webhook; Telegram uses long polling.
143
145
 
144
146
  Telegram bot tokens can be created with:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arisa",
3
- "version": "4.0.24",
3
+ "version": "4.1.4",
4
4
  "description": "Telegram + Pi Agent modular assistant",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -1,13 +1,26 @@
1
1
  import path from "node:path";
2
- import { unlink } from "node:fs/promises";
2
+ import { stat, unlink } from "node:fs/promises";
3
3
  import { createAgentSession, SessionManager, defineTool } from "@earendil-works/pi-coding-agent";
4
4
  import { Type } from "@sinclair/typebox";
5
5
  import { createPiRuntime, hasProviderAuth } from "./pi-runtime.js";
6
6
  import { arisaInstallDir, buildAgentRuntimeContext } from "./runtime-context.js";
7
7
  import { withTimeout } from "./prompt-timeout.js";
8
+ import { buildPiToolPolicy, getCoreCodingTools } from "./core-tools.js";
9
+ import { createSystemShellTool } from "./system-shell-tool.js";
8
10
  import { arisaHomeDir, getChatPiSessionsDir } from "../../runtime/paths.js";
9
11
 
10
12
  const piValidationTimeoutMs = 60_000;
13
+ const arisaToolNames = [
14
+ "list_tools",
15
+ "tool_help",
16
+ "tool_skills",
17
+ "set_tool_config",
18
+ "run_tool",
19
+ "list_scheduled_tasks",
20
+ "cancel_scheduled_task",
21
+ "cancel_all_scheduled_tasks",
22
+ "send_media_reply"
23
+ ];
11
24
 
12
25
  function isLocalBaseUrl(value) {
13
26
  if (typeof value !== "string" || !value.trim()) return false;
@@ -68,6 +81,13 @@ function selectMediaReplyTool(toolRegistry) {
68
81
  return tools.find(looksLikeTextToSpeechTool) || tools[0] || null;
69
82
  }
70
83
 
84
+ async function assertDirectory(dir, label) {
85
+ const stats = await stat(dir);
86
+ if (!stats.isDirectory()) {
87
+ throw new Error(`${label} is not a directory: ${dir}`);
88
+ }
89
+ }
90
+
71
91
  export class AgentManager {
72
92
  constructor({ config, artifactStore, toolRegistry, taskStore, logger }) {
73
93
  this.config = config;
@@ -95,15 +115,15 @@ export class AgentManager {
95
115
  this.sessions.delete(String(chatId));
96
116
  }
97
117
 
98
- createSessionManager(chatId) {
118
+ createSessionManager(chatId, workspaceDir = arisaInstallDir) {
99
119
  const sessionKey = String(chatId);
100
120
  const sessionDir = getChatPiSessionsDir(sessionKey);
101
121
  if (this.pendingNewSessions.has(sessionKey)) {
102
122
  this.logger?.log("agent", `starting new persisted session for chat ${sessionKey}`);
103
- return { sessionManager: SessionManager.create(arisaInstallDir, sessionDir), isNewSession: true };
123
+ return { sessionManager: SessionManager.create(workspaceDir, sessionDir), isNewSession: true };
104
124
  }
105
125
  this.logger?.log("agent", `recovering persisted session for chat ${sessionKey}`);
106
- return { sessionManager: SessionManager.continueRecent(arisaInstallDir, sessionDir), isNewSession: false };
126
+ return { sessionManager: SessionManager.continueRecent(workspaceDir, sessionDir), isNewSession: false };
107
127
  }
108
128
 
109
129
  async validatePiAgent() {
@@ -156,23 +176,36 @@ export class AgentManager {
156
176
  throw new Error(`No auth found for ${this.config.pi.provider}. Re-run bootstrap and complete login for this provider before Telegram starts.`);
157
177
  }
158
178
 
159
- const { sessionManager, isNewSession } = this.createSessionManager(sessionKey);
179
+ const policy = buildPiToolPolicy({
180
+ config: this.config,
181
+ customToolNames: [...arisaToolNames, "system_shell"]
182
+ });
183
+ await assertDirectory(policy.workspaceDir, "pi.workspaceDir");
184
+ const { sessionManager, isNewSession } = this.createSessionManager(sessionKey, policy.workspaceDir);
160
185
  const hasExistingSession = sessionManager.buildSessionContext().messages.length > 0;
161
186
  this.logger?.log("agent", `${hasExistingSession ? "resuming" : "creating"} session for chat ${sessionKey} with model ${effectiveModelId}`);
162
- const customTools = this.createTools(telegram, chatId);
187
+ const customTools = [
188
+ ...this.createTools(telegram, chatId, policy),
189
+ createSystemShellTool({ workspaceDir: policy.workspaceDir, shell: policy.shell })
190
+ ];
163
191
  const { session } = await createAgentSession({
164
- cwd: arisaInstallDir,
192
+ cwd: policy.workspaceDir,
165
193
  agentDir: arisaHomeDir,
166
194
  authStorage,
167
195
  modelRegistry,
168
196
  model,
197
+ tools: policy.tools,
198
+ excludeTools: policy.excludeTools,
169
199
  customTools,
170
200
  sessionManager
171
201
  });
172
202
 
173
203
  if (!hasExistingSession) {
174
204
  this.logger?.log("agent", `created new session for chat ${sessionKey}`);
175
- this.logger?.log("agent", `runtime context for chat ${sessionKey}:\n${buildAgentRuntimeContext()}`);
205
+ this.logger?.log("agent", `runtime context for chat ${sessionKey}:\n${buildAgentRuntimeContext({
206
+ workspaceDir: policy.workspaceDir,
207
+ coreTools: policy.coreTools
208
+ })}`);
176
209
  }
177
210
 
178
211
  const ctx = { session, modelId: effectiveModelId };
@@ -226,20 +259,44 @@ export class AgentManager {
226
259
  return result;
227
260
  }
228
261
 
229
- createTools(telegram, chatId) {
262
+ createTools(telegram, chatId, policy = buildPiToolPolicy({ config: this.config, customToolNames: arisaToolNames })) {
230
263
  const chatArtifactStore = this.artifactStore.forChat(chatId);
231
264
 
232
265
  return [
233
266
  defineTool({
234
267
  name: "list_tools",
235
268
  label: "List tools",
236
- description: "List registered CLI tools and their capabilities.",
269
+ description: "List Arisa core, native shell, and modular CLI tools with their capabilities.",
237
270
  parameters: Type.Object({}),
238
271
  execute: async () => {
239
272
  await this.toolRegistry.load();
273
+ const coreTools = getCoreCodingTools({
274
+ tools: policy.tools,
275
+ excludeTools: policy.excludeTools
276
+ });
277
+ const nativeTools = [{
278
+ name: "system_shell",
279
+ source: "arisa-native",
280
+ description: "Run native system shell commands in the active Arisa workspace.",
281
+ workspaceDir: policy.workspaceDir,
282
+ shell: policy.shell.shellPath || (process.platform === "win32" ? "powershell" : "sh"),
283
+ enabled: !(policy.excludeTools || []).includes("system_shell")
284
+ }];
285
+ const cliTools = this.toolRegistry.list().map((tool) => ({
286
+ ...tool,
287
+ source: "arisa-modular",
288
+ invocation: "run_tool"
289
+ }));
290
+ const result = {
291
+ workspaceDir: policy.workspaceDir,
292
+ coreTools,
293
+ nativeTools,
294
+ cliTools,
295
+ tools: [...coreTools.filter((tool) => tool.enabled), ...nativeTools.filter((tool) => tool.enabled), ...cliTools]
296
+ };
240
297
  return {
241
- content: [{ type: "text", text: JSON.stringify(this.toolRegistry.list(), null, 2) }],
242
- details: { tools: this.toolRegistry.list() }
298
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
299
+ details: result
243
300
  };
244
301
  }
245
302
  }),
@@ -0,0 +1,139 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { arisaHomeDir } from "../../runtime/paths.js";
4
+
5
+ const defaultShellTimeoutMs = 60_000;
6
+
7
+ export const coreCodingToolCatalog = [
8
+ {
9
+ name: "read",
10
+ source: "pi-builtin",
11
+ description: "Read files and supported images from the active workspace.",
12
+ defaultEnabled: true
13
+ },
14
+ {
15
+ name: "bash",
16
+ source: "pi-builtin",
17
+ description: "Run bash-compatible commands from the active workspace.",
18
+ defaultEnabled: true
19
+ },
20
+ {
21
+ name: "edit",
22
+ source: "pi-builtin",
23
+ description: "Patch existing files in the active workspace.",
24
+ defaultEnabled: true
25
+ },
26
+ {
27
+ name: "write",
28
+ source: "pi-builtin",
29
+ description: "Create or overwrite files in the active workspace.",
30
+ defaultEnabled: true
31
+ },
32
+ {
33
+ name: "grep",
34
+ source: "pi-builtin",
35
+ description: "Search file contents from the active workspace.",
36
+ defaultEnabled: false
37
+ },
38
+ {
39
+ name: "find",
40
+ source: "pi-builtin",
41
+ description: "Find files from the active workspace.",
42
+ defaultEnabled: false
43
+ },
44
+ {
45
+ name: "ls",
46
+ source: "pi-builtin",
47
+ description: "List directories from the active workspace.",
48
+ defaultEnabled: false
49
+ }
50
+ ];
51
+
52
+ function unique(values) {
53
+ return [...new Set(values)];
54
+ }
55
+
56
+ function expandHomeDir(value) {
57
+ if (typeof value !== "string") return value;
58
+ if (value === "~") return os.homedir();
59
+ if (value.startsWith("~/") || value.startsWith("~\\")) {
60
+ return path.join(os.homedir(), value.slice(2));
61
+ }
62
+ return value;
63
+ }
64
+
65
+ export function normalizeToolList(value) {
66
+ if (Array.isArray(value)) {
67
+ const tools = value
68
+ .map((item) => String(item || "").trim())
69
+ .filter(Boolean);
70
+ return tools.length ? unique(tools) : undefined;
71
+ }
72
+
73
+ if (typeof value === "string") {
74
+ const tools = value
75
+ .split(",")
76
+ .map((item) => item.trim())
77
+ .filter(Boolean);
78
+ return tools.length ? unique(tools) : undefined;
79
+ }
80
+
81
+ return undefined;
82
+ }
83
+
84
+ function normalizePositiveInteger(value, fallback) {
85
+ const number = Number(value);
86
+ if (!Number.isFinite(number) || number <= 0) return fallback;
87
+ return Math.floor(number);
88
+ }
89
+
90
+ export function resolveWorkspaceDir(value, defaultWorkspaceDir = arisaHomeDir) {
91
+ const raw = typeof value === "string" && value.trim()
92
+ ? value.trim()
93
+ : defaultWorkspaceDir;
94
+ return path.resolve(expandHomeDir(raw));
95
+ }
96
+
97
+ function buildAllowedTools({ configuredTools, customToolNames }) {
98
+ if (!configuredTools) return undefined;
99
+ return unique([...configuredTools, ...customToolNames]);
100
+ }
101
+
102
+ export function getCoreCodingTools({ tools, excludeTools } = {}) {
103
+ const allowed = tools ? new Set(tools) : null;
104
+ const excluded = new Set(excludeTools || []);
105
+
106
+ return coreCodingToolCatalog.map((tool) => {
107
+ const enabled = allowed
108
+ ? allowed.has(tool.name)
109
+ : tool.defaultEnabled;
110
+ return {
111
+ ...tool,
112
+ enabled: enabled && !excluded.has(tool.name)
113
+ };
114
+ });
115
+ }
116
+
117
+ export function buildPiToolPolicy({
118
+ config,
119
+ customToolNames = [],
120
+ defaultWorkspaceDir = arisaHomeDir
121
+ } = {}) {
122
+ const configuredTools = normalizeToolList(config?.pi?.tools);
123
+ const excludeTools = normalizeToolList(config?.pi?.excludeTools);
124
+ const tools = buildAllowedTools({ configuredTools, customToolNames });
125
+ const workspaceDir = resolveWorkspaceDir(config?.pi?.workspaceDir, defaultWorkspaceDir);
126
+
127
+ return {
128
+ workspaceDir,
129
+ tools,
130
+ excludeTools,
131
+ coreTools: getCoreCodingTools({ tools, excludeTools }),
132
+ shell: {
133
+ shellPath: typeof config?.pi?.shellPath === "string" && config.pi.shellPath.trim()
134
+ ? config.pi.shellPath.trim()
135
+ : "",
136
+ timeoutMs: normalizePositiveInteger(config?.pi?.shellTimeoutMs, defaultShellTimeoutMs)
137
+ }
138
+ };
139
+ }
@@ -1,15 +1,26 @@
1
1
  import { fileURLToPath } from "node:url";
2
- import { arisaHomeDir, chatsDir, stateDir, toolStateDir, toolsDir } from "../../runtime/paths.js";
2
+ import { arisaHomeDir, arisaPackageDir, chatsDir, stateDir, toolStateDir, toolsDir } from "../../runtime/paths.js";
3
3
 
4
4
  export const arisaInstallDir = fileURLToPath(new URL("../../..", import.meta.url));
5
5
 
6
- export function buildAgentRuntimeContext() {
6
+ function formatCoreTools(coreTools = []) {
7
+ const enabled = coreTools
8
+ .filter((tool) => tool.enabled)
9
+ .map((tool) => tool.name);
10
+ return enabled.length ? enabled.join(", ") : "(none)";
11
+ }
12
+
13
+ export function buildAgentRuntimeContext({ workspaceDir = arisaHomeDir, coreTools = [] } = {}) {
7
14
  return [
15
+ `workspaceDir: ${workspaceDir}`,
8
16
  `arisaHomeDir: ${arisaHomeDir}`,
17
+ `arisaPackageDir: ${arisaPackageDir}`,
9
18
  `arisaInstallDir: ${arisaInstallDir}`,
10
19
  `userToolsDir: ${toolsDir}`,
11
20
  `toolStateDir: ${toolStateDir}`,
12
21
  `chatsDir: ${chatsDir}`,
13
- `stateDir: ${stateDir}`
22
+ `stateDir: ${stateDir}`,
23
+ `enabledCoreTools: ${formatCoreTools(coreTools)}`,
24
+ "Guidance: create or edit user tools under userToolsDir. Treat arisaPackageDir/arisaInstallDir as Arisa core and modify it only when the user explicitly asks for core changes."
14
25
  ].join("\n");
15
26
  }
@@ -0,0 +1,207 @@
1
+ import path from "node:path";
2
+ import { spawn } from "node:child_process";
3
+ import { access } from "node:fs/promises";
4
+ import { constants, existsSync } from "node:fs";
5
+ import { defineTool } from "@earendil-works/pi-coding-agent";
6
+ import { Type } from "@sinclair/typebox";
7
+
8
+ const maxOutputBytes = 50 * 1024;
9
+ const defaultTimeoutMs = 60_000;
10
+
11
+ function isPowerShell(shellPath) {
12
+ const name = path.basename(shellPath).toLowerCase();
13
+ return name === "powershell.exe" || name === "powershell" || name === "pwsh.exe" || name === "pwsh";
14
+ }
15
+
16
+ function isCmd(shellPath) {
17
+ const name = path.basename(shellPath).toLowerCase();
18
+ return name === "cmd.exe" || name === "cmd";
19
+ }
20
+
21
+ function resolveNativeShell(shellPath = "") {
22
+ if (shellPath) {
23
+ if (isPowerShell(shellPath)) {
24
+ return {
25
+ shell: shellPath,
26
+ label: "powershell",
27
+ argsFor: (command) => ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", command]
28
+ };
29
+ }
30
+ if (isCmd(shellPath)) {
31
+ return {
32
+ shell: shellPath,
33
+ label: "cmd",
34
+ argsFor: (command) => ["/d", "/s", "/c", command]
35
+ };
36
+ }
37
+ return {
38
+ shell: shellPath,
39
+ label: "sh",
40
+ argsFor: (command) => ["-lc", command]
41
+ };
42
+ }
43
+
44
+ if (process.platform === "win32") {
45
+ return {
46
+ shell: "powershell.exe",
47
+ label: "powershell",
48
+ argsFor: (command) => ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", command]
49
+ };
50
+ }
51
+
52
+ return {
53
+ shell: existsSync("/bin/bash") ? "/bin/bash" : "/bin/sh",
54
+ label: "sh",
55
+ argsFor: (command) => ["-lc", command]
56
+ };
57
+ }
58
+
59
+ function appendLimited(current, chunk, state) {
60
+ state.totalBytes += chunk.length;
61
+
62
+ const remaining = maxOutputBytes - state.storedBytes;
63
+ if (remaining <= 0) {
64
+ state.truncated = true;
65
+ return current;
66
+ }
67
+
68
+ const accepted = chunk.length > remaining ? chunk.subarray(0, remaining) : chunk;
69
+ state.storedBytes += accepted.length;
70
+ if (accepted.length < chunk.length) {
71
+ state.truncated = true;
72
+ }
73
+ return current + accepted.toString("utf8");
74
+ }
75
+
76
+ function killProcessTree(child) {
77
+ if (!child.pid) return;
78
+ if (process.platform === "win32") {
79
+ spawn("taskkill", ["/F", "/T", "/PID", String(child.pid)], {
80
+ stdio: "ignore",
81
+ windowsHide: true
82
+ });
83
+ return;
84
+ }
85
+
86
+ try {
87
+ process.kill(-child.pid, "SIGTERM");
88
+ } catch {
89
+ try {
90
+ child.kill("SIGTERM");
91
+ } catch {
92
+ // Process already exited.
93
+ }
94
+ }
95
+ }
96
+
97
+ function formatOutput({ stdout, stderr, exitCode, timedOut, truncated }) {
98
+ const sections = [];
99
+ if (stdout.trim()) sections.push(`stdout:\n${stdout.trimEnd()}`);
100
+ if (stderr.trim()) sections.push(`stderr:\n${stderr.trimEnd()}`);
101
+ if (!sections.length) sections.push(exitCode === 0 && !timedOut ? "(Success, no output)" : "(No output)");
102
+ if (timedOut) sections.push("Command timed out.");
103
+ if (truncated) sections.push(`[Output truncated to ${maxOutputBytes} bytes.]`);
104
+ return sections.join("\n\n");
105
+ }
106
+
107
+ async function runShellCommand({ command, cwd, shellPath, timeoutMs }) {
108
+ await access(cwd, constants.F_OK);
109
+ const nativeShell = resolveNativeShell(shellPath);
110
+ const child = spawn(nativeShell.shell, nativeShell.argsFor(command), {
111
+ cwd,
112
+ detached: process.platform !== "win32",
113
+ stdio: ["ignore", "pipe", "pipe"],
114
+ windowsHide: true
115
+ });
116
+
117
+ let stdout = "";
118
+ let stderr = "";
119
+ const outputState = {
120
+ storedBytes: 0,
121
+ totalBytes: 0,
122
+ truncated: false
123
+ };
124
+
125
+ let timedOut = false;
126
+ const timer = setTimeout(() => {
127
+ timedOut = true;
128
+ killProcessTree(child);
129
+ }, timeoutMs);
130
+
131
+ return new Promise((resolve) => {
132
+ child.stdout.on("data", (chunk) => {
133
+ stdout = appendLimited(stdout, chunk, outputState);
134
+ });
135
+ child.stderr.on("data", (chunk) => {
136
+ stderr = appendLimited(stderr, chunk, outputState);
137
+ });
138
+ child.on("error", (error) => {
139
+ clearTimeout(timer);
140
+ resolve({
141
+ ok: false,
142
+ error,
143
+ stdout,
144
+ stderr,
145
+ shell: nativeShell,
146
+ timedOut,
147
+ truncated: outputState.truncated,
148
+ totalBytes: outputState.totalBytes
149
+ });
150
+ });
151
+ child.on("close", (exitCode) => {
152
+ clearTimeout(timer);
153
+ resolve({
154
+ ok: !timedOut && exitCode === 0,
155
+ exitCode,
156
+ stdout,
157
+ stderr,
158
+ shell: nativeShell,
159
+ timedOut,
160
+ truncated: outputState.truncated,
161
+ totalBytes: outputState.totalBytes
162
+ });
163
+ });
164
+ });
165
+ }
166
+
167
+ export function createSystemShellTool({ workspaceDir, shell = {} }) {
168
+ return defineTool({
169
+ name: "system_shell",
170
+ label: "System Shell",
171
+ description: "Run a command in the active Arisa workspace using the native system shell: PowerShell on Windows, and sh/bash-compatible shell on Unix.",
172
+ parameters: Type.Object({
173
+ command: Type.String({ description: "Command to execute in the active workspace." }),
174
+ timeoutMs: Type.Optional(Type.Number({ description: "Optional timeout in milliseconds for this command." }))
175
+ }),
176
+ execute: async (_id, params) => {
177
+ const timeoutMs = Number.isFinite(Number(params.timeoutMs)) && Number(params.timeoutMs) > 0
178
+ ? Math.floor(Number(params.timeoutMs))
179
+ : (shell.timeoutMs || defaultTimeoutMs);
180
+ const result = await runShellCommand({
181
+ command: params.command,
182
+ cwd: workspaceDir,
183
+ shellPath: shell.shellPath,
184
+ timeoutMs
185
+ });
186
+ const details = {
187
+ stdout: result.stdout,
188
+ stderr: result.stderr,
189
+ exitCode: result.exitCode ?? null,
190
+ shell: result.shell.label,
191
+ shellPath: result.shell.shell,
192
+ cwd: workspaceDir,
193
+ timedOut: result.timedOut,
194
+ truncated: result.truncated,
195
+ totalBytes: result.totalBytes
196
+ };
197
+ if (result.error) {
198
+ details.error = result.error.message;
199
+ }
200
+ return {
201
+ content: [{ type: "text", text: result.error?.message || formatOutput(result) }],
202
+ details,
203
+ isError: !result.ok
204
+ };
205
+ }
206
+ });
207
+ }
package/src/index.js CHANGED
@@ -69,8 +69,20 @@ function toNestedOverrides(nestedFlags) {
69
69
 
70
70
  function toServiceRunnerArgs(nestedFlags) {
71
71
  const args = [];
72
- if (nestedFlags["pi.model"]) {
73
- args.push("--pi.model", nestedFlags["pi.model"]);
72
+ const serviceSafePiFlags = [
73
+ "pi.provider",
74
+ "pi.model",
75
+ "pi.workspaceDir",
76
+ "pi.tools",
77
+ "pi.excludeTools",
78
+ "pi.shellPath",
79
+ "pi.shellTimeoutMs"
80
+ ];
81
+
82
+ for (const flag of serviceSafePiFlags) {
83
+ if (nestedFlags[flag]) {
84
+ args.push(`--${flag}`, nestedFlags[flag]);
85
+ }
74
86
  }
75
87
  return args;
76
88
  }
@@ -104,14 +116,34 @@ async function startRuntimeApp() {
104
116
  await app.start();
105
117
  }
106
118
 
119
+ async function startBackgroundService() {
120
+ const result = await startService({ verbose, cliArgs: toServiceRunnerArgs(cli.nestedFlags) });
121
+ if (!result.ok) {
122
+ console.log(`Arisa is already running in background (pid ${result.pid}).`);
123
+ return result;
124
+ }
125
+ console.log(`Arisa started in background (pid ${result.pid}).`);
126
+ console.log(`Log file: ${result.logFile}`);
127
+ return result;
128
+ }
129
+
107
130
  async function runForeground() {
108
131
  const hasRuntimePiOverrides = Boolean(
109
132
  runtimeOverrides?.pi?.model
110
133
  || runtimeOverrides?.pi?.provider
111
134
  || runtimeOverrides?.pi?.apiKey
135
+ || runtimeOverrides?.pi?.workspaceDir
136
+ || runtimeOverrides?.pi?.tools
137
+ || runtimeOverrides?.pi?.excludeTools
138
+ || runtimeOverrides?.pi?.shellPath
139
+ || runtimeOverrides?.pi?.shellTimeoutMs
112
140
  );
113
141
  logger.log("app", `starting${verbose ? " in verbose mode" : ""}`);
114
- await bootstrapIfNeeded({ force: forceBootstrap });
142
+ const bootstrapResult = await bootstrapIfNeeded({ force: forceBootstrap });
143
+ if (bootstrapResult.startInBackground) {
144
+ await startBackgroundService();
145
+ return;
146
+ }
115
147
  try {
116
148
  await startRuntimeApp();
117
149
  } catch (error) {
@@ -126,7 +158,11 @@ async function runForeground() {
126
158
  throw error;
127
159
  }
128
160
  console.log("Reopening bootstrap so you can provide a Pi API key or switch to a provider you already authenticated with.\n");
129
- await bootstrapIfNeeded({ force: true });
161
+ const retryBootstrapResult = await bootstrapIfNeeded({ force: true });
162
+ if (retryBootstrapResult.startInBackground) {
163
+ await startBackgroundService();
164
+ return;
165
+ }
130
166
  await startRuntimeApp();
131
167
  return;
132
168
  }
@@ -142,14 +178,12 @@ async function main() {
142
178
  }
143
179
 
144
180
  if (command === "start") {
145
- await bootstrapIfNeeded({ force: forceBootstrap });
146
- const result = await startService({ verbose, cliArgs: toServiceRunnerArgs(cli.nestedFlags) });
147
- if (!result.ok) {
148
- console.log(`Arisa is already running in background (pid ${result.pid}).`);
181
+ const bootstrapResult = await bootstrapIfNeeded({ force: forceBootstrap });
182
+ if (bootstrapResult.configCreated && bootstrapResult.viaTelegram && !bootstrapResult.startInBackground) {
183
+ console.log("Config saved. Arisa was not started in background.");
149
184
  return;
150
185
  }
151
- console.log(`Arisa started in background (pid ${result.pid}).`);
152
- console.log(`Log file: ${result.logFile}`);
186
+ await startBackgroundService();
153
187
  return;
154
188
  }
155
189