@saadnvd1/agent-os 0.3.810 → 0.3.817

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 (59) hide show
  1. package/README.md +3 -2
  2. package/app/api/files/upload-temp/route.test.ts +55 -3
  3. package/app/api/files/upload-temp/route.ts +15 -10
  4. package/app/api/reach/probe/route.test.ts +22 -0
  5. package/app/api/reach/probe/route.ts +12 -0
  6. package/app/api/reach/route.ts +30 -0
  7. package/app/api/sessions/list-size.test.ts +77 -0
  8. package/app/api/sessions/route.ts +21 -10
  9. package/app/api/tasks/list-size.test.ts +50 -0
  10. package/app/api/tasks/route.ts +20 -3
  11. package/components/Chat/FileChip.tsx +8 -4
  12. package/components/Chat/ViewSwitch.tsx +2 -1
  13. package/components/Chat/composer/paste.ts +5 -2
  14. package/components/Chat/composer/useAttachFiles.ts +8 -5
  15. package/components/Devices/NetworkSection.tsx +2 -0
  16. package/components/Pane/index.tsx +4 -2
  17. package/components/Reach/ReachRow.tsx +137 -0
  18. package/components/Settings/SetupSection.tsx +2 -0
  19. package/components/Welcome/ConnectBlock.tsx +2 -0
  20. package/content/tips.json +10 -0
  21. package/content/whats-new.json +40 -0
  22. package/data/devices/queries.ts +6 -1
  23. package/data/reach/index.ts +33 -0
  24. package/lib/attachments/check.ts +2 -5
  25. package/lib/attachments/har.ts +122 -0
  26. package/lib/attachments/message.ts +24 -2
  27. package/lib/attachments/policy.ts +104 -9
  28. package/lib/attachments/prepare.ts +100 -0
  29. package/lib/attachments/store.ts +7 -1
  30. package/lib/config/client-config-types.ts +2 -0
  31. package/lib/connect/serve.ts +22 -3
  32. package/lib/connect/supervise.ts +33 -9
  33. package/lib/db/queries.ts +31 -4
  34. package/lib/db/types.ts +3 -1
  35. package/lib/guides.ts +6 -0
  36. package/lib/hosts/remote-menu.ts +4 -2
  37. package/lib/reach/check-cli.ts +117 -0
  38. package/lib/reach/checkReach.ts +62 -0
  39. package/lib/reach/classify.ts +283 -0
  40. package/lib/reach/cli.ts +38 -0
  41. package/lib/reach/gatherFacts.ts +167 -0
  42. package/lib/reach/index.ts +4 -0
  43. package/lib/reach/probeConnect.ts +122 -0
  44. package/lib/reach/types.ts +96 -0
  45. package/lib/security/auth.ts +2 -0
  46. package/lib/security/demo.ts +2 -0
  47. package/lib/sessions/has-prompt.ts +7 -0
  48. package/lib/sessions/omit-null.ts +8 -0
  49. package/lib/tasks/contract/views.ts +12 -0
  50. package/lib/tasks/index.ts +9 -5
  51. package/lib/tasks/session.ts +15 -1
  52. package/lib/tips/view.ts +2 -4
  53. package/package.json +1 -1
  54. package/scripts/agent-os +3 -0
  55. package/scripts/install.sh +12 -1
  56. package/scripts/lib/commands.sh +59 -65
  57. package/scripts/lib/service.sh +230 -0
  58. package/scripts/lib/setup.sh +303 -0
  59. package/scripts/lib/setup.test.ts +389 -0
@@ -0,0 +1,389 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import {
3
+ chmodSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ rmSync,
8
+ symlinkSync,
9
+ writeFileSync,
10
+ } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
13
+ import { tempDir } from "@/lib/test-temp";
14
+
15
+ // `agent-os setup` and the service commands against a fake $HOME. launchctl,
16
+ // systemctl, curl and the TypeScript entry points are fakes that record what
17
+ // they were asked; the server is "up" once a file exists.
18
+ const LIB = join(import.meta.dirname);
19
+ const TOOLS = [
20
+ "tr",
21
+ "cat",
22
+ "sed",
23
+ "head",
24
+ "grep",
25
+ "mkdir",
26
+ "id",
27
+ "mktemp",
28
+ "cp",
29
+ "rm",
30
+ "dirname",
31
+ "realpath",
32
+ "ps",
33
+ "stat",
34
+ "mv",
35
+ "touch",
36
+ "nohup",
37
+ ];
38
+
39
+ let dir: string;
40
+
41
+ function fake(name: string, body: string) {
42
+ const p = join(dir, "bin", name);
43
+ writeFileSync(p, `#!/bin/sh\n${body}\n`);
44
+ chmodSync(p, 0o755);
45
+ }
46
+ const calls = () =>
47
+ existsSync(join(dir, "calls.log"))
48
+ ? readFileSync(join(dir, "calls.log"), "utf8")
49
+ : "";
50
+ const plist = () => join(dir, "Library/LaunchAgents/com.agent-os.plist");
51
+ const unit = () => join(dir, ".config/systemd/user/agent-os.service");
52
+ const script = () => join(dir, "agent-os");
53
+
54
+ function run(command: string, os = "macos") {
55
+ return spawnSync(
56
+ "/bin/bash",
57
+ [
58
+ "-euo",
59
+ "pipefail",
60
+ "-c",
61
+ [
62
+ `source "${LIB}/common.sh"`,
63
+ `source "${LIB}/prerequisites.sh"`,
64
+ `source "${LIB}/commands.sh"`,
65
+ `source "${LIB}/setup.sh"`,
66
+ `BOLD=""; OS=${os} WSL=0 PORT=3011 AGENT_OS_HOME="${dir}" REPO_DIR="${dir}/repo"`,
67
+ `PID_FILE="${dir}/agent-os.pid" LOG_FILE="${dir}/agent-os.log" SCRIPT_DIR="${LIB}"`,
68
+ `has_systemd() { [[ "$OS" != macos ]]; }`,
69
+ `enable_linger() { echo "linger" >> "${dir}/calls.log"; }`,
70
+ `aos_tsx() { echo "tsx $*" >> "${dir}/calls.log"; return "$(cat "${dir}/check-exit" 2>/dev/null || echo 0)"; }`,
71
+ command,
72
+ ].join("; "),
73
+ script(),
74
+ ],
75
+ {
76
+ cwd: dir,
77
+ encoding: "utf8",
78
+ env: {
79
+ PATH: join(dir, "bin"),
80
+ HOME: dir,
81
+ USER: "tester",
82
+ NODE_ENV: "test",
83
+ },
84
+ }
85
+ );
86
+ }
87
+
88
+ beforeEach(() => {
89
+ dir = tempDir("aos-setup-");
90
+ mkdirSync(join(dir, "bin"));
91
+ mkdirSync(join(dir, "repo/.next"), { recursive: true });
92
+ writeFileSync(join(dir, "repo/.next/BUILD_ID"), "x");
93
+ writeFileSync(script(), "");
94
+ for (const tool of [...TOOLS, "node"]) {
95
+ const real =
96
+ tool === "node"
97
+ ? process.execPath
98
+ : ["/usr/bin", "/bin"].map((d) => join(d, tool)).find(existsSync);
99
+ if (real) symlinkSync(real, join(dir, "bin", tool));
100
+ }
101
+ fake("sleep", "true");
102
+ // launchctl: print succeeds once bootstrapped; bootstrap starts the server.
103
+ fake(
104
+ "launchctl",
105
+ `echo "launchctl $*" >> "${dir}/calls.log"
106
+ case "$1" in
107
+ print) [ -f "${dir}/loaded" ];;
108
+ bootstrap) [ -f "${dir}/no-gui" ] && exit 5; touch "${dir}/loaded" "${dir}/up";;
109
+ bootout) rm -f "${dir}/loaded" "${dir}/up";;
110
+ kickstart) touch "${dir}/up";;
111
+ esac`
112
+ );
113
+ fake(
114
+ "systemctl",
115
+ `echo "systemctl $*" >> "${dir}/calls.log"
116
+ case "$*" in *is-enabled*|*is-active*) exit 1;; esac`
117
+ );
118
+ fake(
119
+ "curl",
120
+ `for a in "$@"; do last="$a"; done
121
+ case "$*" in
122
+ *"-X PUT"*) echo "curl PUT $last" >> "${dir}/calls.log"; exit 0;;
123
+ esac
124
+ [ -f "${dir}/up" ] || exit 7
125
+ case "$last" in
126
+ */api/health) echo '{"ok":true,"startedAt":"2026-10-31T12:00:00Z"}';;
127
+ */api/devices/network) cat "${dir}/net.json" 2>/dev/null || echo '{}';;
128
+ esac`
129
+ );
130
+ // Stays up a moment, as a server would, for cmd_start's liveness check.
131
+ fake(
132
+ "npm",
133
+ `echo "npm $*" >> "${dir}/calls.log"; touch "${dir}/up"; exec /bin/sleep 1`
134
+ );
135
+ });
136
+ afterEach(() => rmSync(dir, { recursive: true, force: true }));
137
+
138
+ const PLIST = (
139
+ s: string,
140
+ log: string
141
+ ) => `<?xml version="1.0" encoding="UTF-8"?>
142
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
143
+ <plist version="1.0">
144
+ <dict>
145
+ <key>Label</key>
146
+ <string>com.agent-os</string>
147
+ <key>ProgramArguments</key>
148
+ <array>
149
+ <string>${s}</string>
150
+ <string>start-foreground</string>
151
+ </array>
152
+ <key>RunAtLoad</key>
153
+ <true/>
154
+ <key>KeepAlive</key>
155
+ <dict>
156
+ <key>SuccessfulExit</key>
157
+ <false/>
158
+ </dict>
159
+ <key>ThrottleInterval</key>
160
+ <integer>10</integer>
161
+ <key>AbandonProcessGroup</key>
162
+ <true/>
163
+ <key>StandardOutPath</key>
164
+ <string>${log}</string>
165
+ <key>StandardErrorPath</key>
166
+ <string>${log}</string>
167
+ <key>EnvironmentVariables</key>
168
+ <dict>
169
+ <key>PATH</key>
170
+ <string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
171
+ </dict>
172
+ </dict>
173
+ </plist>
174
+ `;
175
+
176
+ // What `agent-os enable` wrote before this change, byte for byte.
177
+ const UNIT = (s: string) => `[Unit]
178
+ Description=AgentOS - AI Coding Session Manager
179
+ After=network.target
180
+
181
+ [Service]
182
+ Type=simple
183
+ ExecStart=${s} start-foreground
184
+ Restart=on-failure
185
+ RestartSec=10
186
+ # Only the server is stopped on a restart: terminals (tmux), chat workers and
187
+ # an update in progress outlive it, as they do under launchd.
188
+ KillMode=process
189
+ Environment=PATH=/usr/local/bin:/usr/bin:/bin
190
+
191
+ [Install]
192
+ WantedBy=default.target
193
+ `;
194
+
195
+ const setup = (args = "--yes --via lan --no-pair") => run(`cmd_setup ${args}`);
196
+ const realScript = () => run(`realpath "$0"`).stdout.trim();
197
+
198
+ describe("agent-os setup", () => {
199
+ it("installs the service, turns on Wi-Fi access and runs the check", () => {
200
+ const r = setup();
201
+ expect(r.status, r.stderr).toBe(0);
202
+ expect(readFileSync(plist(), "utf8")).toBe(
203
+ PLIST(realScript(), join(dir, "agent-os.log"))
204
+ );
205
+ const log = calls();
206
+ expect(log).toMatch(
207
+ /launchctl bootout gui\/\d+\/com\.agent-os\nlaunchctl bootstrap gui\/\d+ /
208
+ );
209
+ expect(log).not.toMatch(/launchctl (load|unload)/);
210
+ expect(log).toContain("curl PUT http://127.0.0.1:3011/api/devices/network");
211
+ expect(log).toContain("tsx lib/reach/cli.ts 3011 --via lan");
212
+ });
213
+
214
+ it("a second run changes nothing", () => {
215
+ setup();
216
+ writeFileSync(join(dir, "net.json"), '{"lan":{"on":true}}');
217
+ rmSync(join(dir, "calls.log"));
218
+ const r = setup();
219
+ expect(r.status, r.stderr).toBe(0);
220
+ expect(r.stdout).toContain(
221
+ "Starts at login and restarts after a crash (already done)"
222
+ );
223
+ expect(r.stdout).toContain(
224
+ "Devices on this Wi-Fi can connect (already done)"
225
+ );
226
+ expect(calls()).not.toMatch(/bootstrap|bootout|PUT/);
227
+ });
228
+
229
+ it("never overwrites a service file you changed under --yes", () => {
230
+ mkdirSync(join(dir, "Library/LaunchAgents"), { recursive: true });
231
+ writeFileSync(plist(), "<plist>mine</plist>\n");
232
+ const r = setup();
233
+ expect(r.stdout).toContain("Kept yours");
234
+ expect(readFileSync(plist(), "utf8")).toBe("<plist>mine</plist>\n");
235
+ expect(calls()).not.toContain("bootstrap");
236
+ });
237
+
238
+ it("leaves a server started some other way alone", () => {
239
+ writeFileSync(join(dir, "up"), "");
240
+ const r = setup();
241
+ expect(r.status, r.stderr).toBe(0);
242
+ expect(r.stdout).toContain("started some other way");
243
+ expect(existsSync(plist())).toBe(false);
244
+ expect(calls()).not.toContain("launchctl bootstrap");
245
+ });
246
+
247
+ it("stops at the check's failure, and says to run it again", () => {
248
+ writeFileSync(join(dir, "check-exit"), "2");
249
+ const r = setup();
250
+ expect(r.status).toBe(1);
251
+ expect(r.stdout).toContain("Run 'agent-os setup' again");
252
+ expect(r.stdout).not.toContain("5. Your phone");
253
+ });
254
+
255
+ it("carries on without launchd when there's no desktop session (ssh)", () => {
256
+ writeFileSync(join(dir, "no-gui"), "");
257
+ const r = setup();
258
+ expect(r.status, r.stdout + r.stderr).toBe(0);
259
+ expect(r.stdout).toContain("no-gui-session");
260
+ expect(calls()).toContain("npm start");
261
+ });
262
+
263
+ it("refuses --via with nothing after it", () => {
264
+ const r = run("cmd_setup --via");
265
+ expect(r.status).toBe(2);
266
+ expect(r.stdout + r.stderr).toContain(
267
+ "--via takes connect, tailscale or lan"
268
+ );
269
+ });
270
+
271
+ it("refuses an unknown route", () => {
272
+ const r = setup("--via carrier-pigeon");
273
+ expect(r.status).toBe(2);
274
+ expect(r.stdout + r.stderr).toContain(
275
+ "--via takes connect, tailscale or lan"
276
+ );
277
+ });
278
+ });
279
+
280
+ describe("the service file", () => {
281
+ it("systemd: the same unit as before, byte for byte", () => {
282
+ expect(run(`render_systemd_unit "$(realpath "$0")"`, "debian").stdout).toBe(
283
+ UNIT(realScript())
284
+ );
285
+ });
286
+
287
+ it("reconcile: installed, then unchanged", () => {
288
+ expect(run("service_reconcile").stdout.trim()).toBe("installed");
289
+ expect(run("service_reconcile").stdout.trim()).toBe("unchanged");
290
+ });
291
+
292
+ it("reconcile: the old plist is ours and gets updated", () => {
293
+ mkdirSync(join(dir, "Library/LaunchAgents"), { recursive: true });
294
+ writeFileSync(
295
+ plist(),
296
+ run(`render_launchd_plist_v1 "$(realpath "$0")"`).stdout
297
+ );
298
+ expect(run("service_file_state").stdout.trim()).toBe("older");
299
+ expect(run("service_reconcile keep").stdout.trim()).toBe("updated");
300
+ expect(readFileSync(plist(), "utf8")).toContain(
301
+ "<string>start-foreground</string>"
302
+ );
303
+ });
304
+
305
+ it("reconcile: an edited file is kept under keep", () => {
306
+ mkdirSync(join(dir, "Library/LaunchAgents"), { recursive: true });
307
+ writeFileSync(plist(), "edited\n");
308
+ expect(run("service_reconcile keep").stdout.trim()).toBe("kept-yours");
309
+ expect(readFileSync(plist(), "utf8")).toBe("edited\n");
310
+ });
311
+
312
+ it("reconcile: an edited file that is replaced is kept as .bak", () => {
313
+ mkdirSync(join(dir, "Library/LaunchAgents"), { recursive: true });
314
+ writeFileSync(plist(), "edited\n");
315
+ const r = run("service_reconcile");
316
+ expect(r.stdout.trim()).toBe("updated");
317
+ expect(readFileSync(`${plist()}.bak`, "utf8")).toBe("edited\n");
318
+ expect(readFileSync(plist(), "utf8")).toContain("start-foreground");
319
+ });
320
+
321
+ it("enable on an edited systemd unit keeps a copy of it", () => {
322
+ mkdirSync(join(dir, ".config/systemd/user"), { recursive: true });
323
+ writeFileSync(unit(), "[Service]\nExecStart=/mine\n");
324
+ const r = run("cmd_enable", "debian");
325
+ expect(r.status, r.stderr).toBe(0);
326
+ expect(readFileSync(`${unit()}.bak`, "utf8")).toBe(
327
+ "[Service]\nExecStart=/mine\n"
328
+ );
329
+ expect(readFileSync(unit(), "utf8")).toBe(UNIT(realScript()));
330
+ });
331
+
332
+ it("paths with odd characters stay one value in both files", () => {
333
+ const odd = "/Users/a b/R&D <x>/100%/agent-os";
334
+ const xml = run(`render_launchd_plist "${odd}"`).stdout;
335
+ expect(xml).toContain(
336
+ "<string>/Users/a b/R&amp;D &lt;x&gt;/100%/agent-os</string>"
337
+ );
338
+ const systemd = run(`render_systemd_unit '${odd}'`, "debian").stdout;
339
+ expect(systemd).toContain(
340
+ 'ExecStart="/Users/a b/R&D <x>/100%%/agent-os" start-foreground'
341
+ );
342
+ expect(run(`render_systemd_unit '/a/"q"\\b c'`, "debian").stdout).toContain(
343
+ 'ExecStart="/a/\\"q\\"\\\\b c" start-foreground'
344
+ );
345
+ });
346
+
347
+ it("systemd: enable writes the unit and enables it", () => {
348
+ const r = run("cmd_enable", "debian");
349
+ expect(r.status, r.stderr).toBe(0);
350
+ expect(readFileSync(unit(), "utf8")).toBe(UNIT(realScript()));
351
+ expect(calls()).toContain("systemctl --user enable agent-os");
352
+ });
353
+ });
354
+
355
+ describe("launchd commands", () => {
356
+ it("stop boots it out, restart kickstarts it, disable boots out and removes it", () => {
357
+ run("cmd_enable");
358
+ rmSync(join(dir, "calls.log"));
359
+ run("cmd_restart");
360
+ expect(calls()).toMatch(/launchctl kickstart -k gui\/\d+\/com\.agent-os/);
361
+ run("cmd_stop");
362
+ expect(calls()).toMatch(/launchctl bootout gui\/\d+\/com\.agent-os/);
363
+ run("cmd_start");
364
+ expect(calls()).toMatch(
365
+ /launchctl bootstrap gui\/\d+ .*com\.agent-os\.plist/
366
+ );
367
+ run("cmd_disable");
368
+ expect(existsSync(plist())).toBe(false);
369
+ expect(calls()).not.toMatch(/launchctl (load|unload)/);
370
+ });
371
+
372
+ it("start with the agent loaded but the server gone never kills one that's running", () => {
373
+ run("cmd_enable");
374
+ rmSync(join(dir, "up"));
375
+ rmSync(join(dir, "calls.log"));
376
+ const r = run("cmd_start");
377
+ expect(r.status, r.stderr).toBe(0);
378
+ expect(calls()).toMatch(/launchctl kickstart gui\/\d+\/com\.agent-os/);
379
+ expect(calls()).not.toContain("kickstart -k");
380
+ });
381
+
382
+ it("enable twice: the second is a no-op", () => {
383
+ run("cmd_enable");
384
+ rmSync(join(dir, "calls.log"));
385
+ const r = run("cmd_enable");
386
+ expect(r.stdout).toContain("already enabled");
387
+ expect(calls()).not.toMatch(/bootstrap|bootout/);
388
+ });
389
+ });