@saadnvd1/agent-os 0.3.807 → 0.3.815
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 +3 -2
- package/app/api/reach/probe/route.test.ts +22 -0
- package/app/api/reach/probe/route.ts +12 -0
- package/app/api/reach/route.ts +30 -0
- package/app/api/sessions/list-size.test.ts +77 -0
- package/app/api/sessions/route.ts +21 -10
- package/app/api/tasks/list-size.test.ts +50 -0
- package/app/api/tasks/route.ts +20 -3
- package/components/Chat/ViewSwitch.tsx +2 -1
- package/components/Devices/NetworkSection.tsx +2 -0
- package/components/Media/MediaViewer.tsx +111 -0
- package/components/Media/viewer.test.ts +26 -0
- package/components/Media/viewer.ts +23 -0
- package/components/Pane/index.tsx +4 -2
- package/components/Reach/ReachRow.tsx +137 -0
- package/components/Settings/SetupSection.tsx +2 -0
- package/components/Tips/TipDialog.tsx +1 -1
- package/components/Tips/TipMedia.tsx +38 -3
- package/components/Updates/{UpdateBanner.test.ts → UpdateToast.test.ts} +158 -8
- package/components/Updates/{UpdateBanner.tsx → UpdateToast.tsx} +119 -89
- package/components/Updates/index.ts +1 -1
- package/components/Welcome/ConnectBlock.tsx +2 -0
- package/components/views/DesktopView.tsx +2 -2
- package/components/views/MobileView.tsx +2 -2
- package/content/tips.json +10 -0
- package/content/whats-new.json +48 -0
- package/data/devices/queries.ts +6 -1
- package/data/reach/index.ts +33 -0
- package/lib/connect/serve.ts +22 -3
- package/lib/connect/supervise.ts +33 -9
- package/lib/db/queries.ts +31 -4
- package/lib/db/types.ts +3 -1
- package/lib/guides.ts +6 -0
- package/lib/hosts/remote-menu.ts +4 -2
- package/lib/reach/check-cli.ts +117 -0
- package/lib/reach/checkReach.ts +62 -0
- package/lib/reach/classify.ts +283 -0
- package/lib/reach/cli.ts +38 -0
- package/lib/reach/gatherFacts.ts +167 -0
- package/lib/reach/index.ts +4 -0
- package/lib/reach/probeConnect.ts +122 -0
- package/lib/reach/types.ts +96 -0
- package/lib/security/auth.ts +2 -0
- package/lib/security/demo.ts +2 -0
- package/lib/sessions/has-prompt.ts +7 -0
- package/lib/sessions/omit-null.ts +8 -0
- package/lib/tasks/contract/views.ts +12 -0
- package/lib/tasks/index.ts +9 -5
- package/lib/tasks/session.ts +15 -1
- package/lib/tips/view.ts +2 -4
- package/package.json +1 -1
- package/scripts/agent-os +3 -0
- package/scripts/install.sh +12 -1
- package/scripts/lib/commands.sh +59 -65
- package/scripts/lib/service.sh +230 -0
- package/scripts/lib/setup.sh +303 -0
- package/scripts/lib/setup.test.ts +389 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Can other devices reach this machine, and if not, why. The facts are plain
|
|
3
|
+
* labels gathered from the machine (gatherFacts.ts); the classifier turns them
|
|
4
|
+
* into findings whose text comes from one table (classify.ts), so the CLI, the
|
|
5
|
+
* web app and the phone app all say the same thing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type ReachRoute = "connect" | "tailscale" | "lan";
|
|
9
|
+
|
|
10
|
+
export const REACH_CAUSES = [
|
|
11
|
+
"no-route",
|
|
12
|
+
"service-down",
|
|
13
|
+
"service-stuck",
|
|
14
|
+
"port-taken",
|
|
15
|
+
"lan-off",
|
|
16
|
+
"firewall",
|
|
17
|
+
"tailscale-missing",
|
|
18
|
+
"tailscale-logged-out",
|
|
19
|
+
"connect-not-linked",
|
|
20
|
+
"connect-off",
|
|
21
|
+
"connect-not-here",
|
|
22
|
+
"connect-denied",
|
|
23
|
+
"connector-not-ready",
|
|
24
|
+
"relay-down",
|
|
25
|
+
"origin-down",
|
|
26
|
+
"wrong-origin",
|
|
27
|
+
"dns",
|
|
28
|
+
"offline",
|
|
29
|
+
"certificate",
|
|
30
|
+
] as const;
|
|
31
|
+
|
|
32
|
+
export type ReachCause = (typeof REACH_CAUSES)[number];
|
|
33
|
+
|
|
34
|
+
export type ReachStatus = "ok" | "warn" | "fail";
|
|
35
|
+
|
|
36
|
+
export interface ReachFinding {
|
|
37
|
+
route?: ReachRoute;
|
|
38
|
+
status: ReachStatus;
|
|
39
|
+
cause?: ReachCause;
|
|
40
|
+
/** "nothing": AgentOS is retrying by itself; "you": the fix line is for you. */
|
|
41
|
+
needs: "you" | "nothing";
|
|
42
|
+
title: string;
|
|
43
|
+
/** One line: a command or what to do. Empty when nothing is needed. */
|
|
44
|
+
fix: string;
|
|
45
|
+
/** Its section of the reachability guide. */
|
|
46
|
+
guide?: string;
|
|
47
|
+
/** The link this route works on, when it works. */
|
|
48
|
+
url?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ReachReport {
|
|
52
|
+
status: ReachStatus;
|
|
53
|
+
/** The best link another device can use right now. */
|
|
54
|
+
url?: string;
|
|
55
|
+
findings: ReachFinding[];
|
|
56
|
+
checkedAt: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** What answered when this machine fetched its own Connect address. */
|
|
60
|
+
export type ConnectProbe =
|
|
61
|
+
| { kind: "ok" }
|
|
62
|
+
| { kind: "wrong-origin" }
|
|
63
|
+
// The edge's own error page: tunnel not connected, or origin unreachable.
|
|
64
|
+
| { kind: "edge"; status: number; tunnel: boolean }
|
|
65
|
+
| { kind: "dns"; scope: "host" | "all" }
|
|
66
|
+
| { kind: "tls"; skewed: boolean }
|
|
67
|
+
| { kind: "offline" }
|
|
68
|
+
| { kind: "timeout" };
|
|
69
|
+
|
|
70
|
+
/** Why this server doesn't run Connect, as a label. */
|
|
71
|
+
export type NotHere =
|
|
72
|
+
"env-off" | "dev-build" | "wrong-port" | "auth-off" | "lock" | "other";
|
|
73
|
+
|
|
74
|
+
export interface ReachFacts {
|
|
75
|
+
now: number;
|
|
76
|
+
port: number;
|
|
77
|
+
lan: { on: boolean; addresses: string[] };
|
|
78
|
+
/** Read-only firewall reading: "blocks" only when it would stop LAN devices. */
|
|
79
|
+
firewall: "off" | "blocks" | "unknown";
|
|
80
|
+
tailscale: {
|
|
81
|
+
state: "running" | "logged-out" | "missing" | "unknown";
|
|
82
|
+
url?: string;
|
|
83
|
+
};
|
|
84
|
+
connect: {
|
|
85
|
+
state: "off" | "refused" | "error" | "connecting" | "up" | "denied";
|
|
86
|
+
linked: boolean;
|
|
87
|
+
enabled: boolean;
|
|
88
|
+
hostname?: string;
|
|
89
|
+
notHere?: NotHere;
|
|
90
|
+
/** Where the last failure came from: fetching the connector, or the service. */
|
|
91
|
+
stage?: "connector" | "service";
|
|
92
|
+
/** When the tunnel was last seen leaving "up" (or first trying). */
|
|
93
|
+
downSince?: number;
|
|
94
|
+
};
|
|
95
|
+
probe?: ConnectProbe;
|
|
96
|
+
}
|
package/lib/security/auth.ts
CHANGED
|
@@ -95,6 +95,8 @@ const PUBLIC_EXACT = new Set([
|
|
|
95
95
|
"/api/peer/hello",
|
|
96
96
|
"/api/peer/hello/reveal",
|
|
97
97
|
"/api/peer/hello/status",
|
|
98
|
+
// A constant per boot, so this machine can tell its own answer through Connect.
|
|
99
|
+
"/api/reach/probe",
|
|
98
100
|
"/manifest.webmanifest",
|
|
99
101
|
"/manifest.json",
|
|
100
102
|
"/favicon.ico",
|
package/lib/security/demo.ts
CHANGED
|
@@ -146,6 +146,8 @@ const READS = [
|
|
|
146
146
|
// stub: what a client checks before it trusts the server.
|
|
147
147
|
"devices",
|
|
148
148
|
"devices/network",
|
|
149
|
+
// A fixed "no way in" report: the demo never probes the internet.
|
|
150
|
+
"reach",
|
|
149
151
|
// Whether this is a demo, for the UI.
|
|
150
152
|
"demo",
|
|
151
153
|
// The changelog: a static file, no agent.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Session } from "@/lib/db/types";
|
|
2
|
+
|
|
3
|
+
// Whether a session is a task (it has a prompt). A list carries has_prompt
|
|
4
|
+
// in place of the prompt's text; a single session carries the text.
|
|
5
|
+
export const hasPrompt = (
|
|
6
|
+
s: Pick<Session, "task_prompt" | "has_prompt">
|
|
7
|
+
): boolean => !!(s.has_prompt ?? s.task_prompt);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// A list row without its null fields: the client's types read a missing field
|
|
2
|
+
// as null, and over half of a session row's columns are.
|
|
3
|
+
export function omitNulls<T extends object>(row: T): T {
|
|
4
|
+
const out: Partial<T> = {};
|
|
5
|
+
for (const k of Object.keys(row) as (keyof T)[])
|
|
6
|
+
if (row[k] !== null) out[k] = row[k];
|
|
7
|
+
return out as T;
|
|
8
|
+
}
|
|
@@ -11,6 +11,17 @@ import type { TaskView } from "../index";
|
|
|
11
11
|
import { viewOf } from "../move-targets";
|
|
12
12
|
import * as client from "./client";
|
|
13
13
|
|
|
14
|
+
// The list leaves out the fields that are null.
|
|
15
|
+
const NULL_FIELDS = {
|
|
16
|
+
projectName: null,
|
|
17
|
+
branch: null,
|
|
18
|
+
baseBranch: null,
|
|
19
|
+
pr: null,
|
|
20
|
+
blocked: null,
|
|
21
|
+
cardUrl: null,
|
|
22
|
+
setup: null,
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
14
25
|
function offlineView(
|
|
15
26
|
session: Session,
|
|
16
27
|
hostName: string,
|
|
@@ -68,6 +79,7 @@ async function viewsOn(hostId: string, rows: Session[]): Promise<TaskView[]> {
|
|
|
68
79
|
}
|
|
69
80
|
return [
|
|
70
81
|
{
|
|
82
|
+
...NULL_FIELDS,
|
|
71
83
|
...t,
|
|
72
84
|
// As this machine mirrored it: the other one may predate views.
|
|
73
85
|
view: viewOf(s),
|
package/lib/tasks/index.ts
CHANGED
|
@@ -67,6 +67,7 @@ import { saidSinceLastMessage } from "../chat/store";
|
|
|
67
67
|
|
|
68
68
|
import { lastActiveOf } from "./last-active";
|
|
69
69
|
import { setPlan, setTask } from "../db/session-parts";
|
|
70
|
+
import { hasPrompt } from "../sessions/has-prompt";
|
|
70
71
|
import { joinCrew } from "../sessions/crew";
|
|
71
72
|
export * from "./state";
|
|
72
73
|
export { codeReviewGate, codeReviewRefusal } from "./code-review";
|
|
@@ -77,7 +78,9 @@ export { assertMovable, moveTask } from "./move-flow";
|
|
|
77
78
|
export interface TaskView {
|
|
78
79
|
id: string;
|
|
79
80
|
name: string;
|
|
80
|
-
|
|
81
|
+
// A list leaves the text out (hasPrompt says there is one).
|
|
82
|
+
prompt?: string;
|
|
83
|
+
hasPrompt?: boolean;
|
|
81
84
|
projectId: string | null;
|
|
82
85
|
projectName: string | null;
|
|
83
86
|
branch: string | null;
|
|
@@ -523,7 +526,8 @@ export async function taskView(session: Session): Promise<TaskView> {
|
|
|
523
526
|
return {
|
|
524
527
|
id: session.id,
|
|
525
528
|
name: session.name,
|
|
526
|
-
prompt: session.task_prompt ||
|
|
529
|
+
prompt: session.task_prompt || undefined,
|
|
530
|
+
hasPrompt: hasPrompt(session),
|
|
527
531
|
projectId: session.project_id,
|
|
528
532
|
projectName: project?.name ?? null,
|
|
529
533
|
branch: session.branch_name,
|
|
@@ -563,9 +567,9 @@ export function movedTasks(): { id: string; movedTo: string | null }[] {
|
|
|
563
567
|
.all() as { id: string; movedTo: string | null }[];
|
|
564
568
|
}
|
|
565
569
|
|
|
566
|
-
// Archived tasks aren't listed.
|
|
567
|
-
export async function listTasks(): Promise<TaskView[]> {
|
|
568
|
-
const sessions = taskSessions(false);
|
|
570
|
+
// Archived tasks aren't listed; mergedLimit keeps only the most recent merged.
|
|
571
|
+
export async function listTasks(mergedLimit?: number): Promise<TaskView[]> {
|
|
572
|
+
const sessions = taskSessions(false, mergedLimit);
|
|
569
573
|
const [local, remote] = await Promise.all([
|
|
570
574
|
Promise.all(sessions.filter((s) => !isMirror(s)).map(taskView)),
|
|
571
575
|
mirrorViews(sessions.filter(isMirror)),
|
package/lib/tasks/session.ts
CHANGED
|
@@ -31,12 +31,26 @@ import { setPr } from "../db/session-parts";
|
|
|
31
31
|
export const expandHome = (p: string) => p.replace(/^~/, os.homedir());
|
|
32
32
|
|
|
33
33
|
// Without the brief and system prompt: a list doesn't need them.
|
|
34
|
-
|
|
34
|
+
// mergedLimit keeps only that many of the most recently finished merged ones.
|
|
35
|
+
export function taskSessions(
|
|
36
|
+
includeArchived = true,
|
|
37
|
+
mergedLimit?: number
|
|
38
|
+
): Session[] {
|
|
39
|
+
const cap =
|
|
40
|
+
mergedLimit === undefined
|
|
41
|
+
? ""
|
|
42
|
+
: `AND (task_status != 'merged' OR id IN (
|
|
43
|
+
SELECT id FROM session_rows
|
|
44
|
+
WHERE task_status = 'merged'
|
|
45
|
+
${includeArchived ? "" : "AND archived_at IS NULL"}
|
|
46
|
+
ORDER BY COALESCE(NULLIF(merged_at, ''), updated_at) DESC
|
|
47
|
+
LIMIT ${Math.max(0, Math.floor(mergedLimit))}))`;
|
|
35
48
|
return db
|
|
36
49
|
.prepare(
|
|
37
50
|
`SELECT ${sessionListColumns(db)} FROM session_rows
|
|
38
51
|
WHERE task_status IS NOT NULL AND task_status != 'moved'
|
|
39
52
|
${includeArchived ? "" : "AND archived_at IS NULL"}
|
|
53
|
+
${cap}
|
|
40
54
|
ORDER BY created_at DESC`
|
|
41
55
|
)
|
|
42
56
|
.all() as Session[];
|
package/lib/tips/view.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { tipApplies } from "./conditions";
|
|
|
3
3
|
import { loadTips } from "./load";
|
|
4
4
|
import { orderDeck, pickToday } from "./pick";
|
|
5
5
|
import type { ClientTip, Tip, TipsView } from "./schema";
|
|
6
|
+
import { guideUrl } from "@/lib/guides";
|
|
6
7
|
import {
|
|
7
8
|
readPinned,
|
|
8
9
|
readSeen,
|
|
@@ -12,10 +13,7 @@ import {
|
|
|
12
13
|
writeSeen,
|
|
13
14
|
} from "./state";
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
export const guideUrl = (g: NonNullable<Tip["guide"]>) =>
|
|
18
|
-
`${GUIDES}/${g.file}.md${g.anchor ? `#${g.anchor}` : ""}`;
|
|
16
|
+
export { guideUrl };
|
|
19
17
|
|
|
20
18
|
const forClient = ({ id, title, body, guide, media }: Tip): ClientTip => ({
|
|
21
19
|
id,
|
package/package.json
CHANGED
package/scripts/agent-os
CHANGED
|
@@ -66,6 +66,7 @@ source "$SCRIPT_DIR/lib/prerequisites.sh"
|
|
|
66
66
|
source "$SCRIPT_DIR/lib/ai-clis.sh"
|
|
67
67
|
source "$SCRIPT_DIR/lib/commands.sh"
|
|
68
68
|
source "$SCRIPT_DIR/lib/machine.sh"
|
|
69
|
+
source "$SCRIPT_DIR/lib/setup.sh"
|
|
69
70
|
|
|
70
71
|
# The package family the installers key off (debian, redhat, linux, macos),
|
|
71
72
|
# and the WSL version (0 off WSL).
|
|
@@ -75,6 +76,8 @@ WSL=$(detect_wsl)
|
|
|
75
76
|
# Main
|
|
76
77
|
case "${1:-}" in
|
|
77
78
|
install) cmd_install "${2:-}" ;;
|
|
79
|
+
setup) shift; cmd_setup "$@" ;;
|
|
80
|
+
check) shift; aos_tsx lib/reach/cli.ts "$PORT" "$@" ;;
|
|
78
81
|
start) cmd_start ;;
|
|
79
82
|
stop) cmd_stop ;;
|
|
80
83
|
restart) cmd_restart ;;
|
package/scripts/install.sh
CHANGED
|
@@ -45,4 +45,15 @@ else
|
|
|
45
45
|
fi
|
|
46
46
|
|
|
47
47
|
# Run the install command
|
|
48
|
-
|
|
48
|
+
"$INSTALL_DIR/scripts/agent-os" install
|
|
49
|
+
|
|
50
|
+
# Under `curl | bash` stdin is this script, so ask on the terminal itself.
|
|
51
|
+
if [[ -t 1 ]] && { : < /dev/tty; } 2> /dev/null; then
|
|
52
|
+
echo ""
|
|
53
|
+
read -p "Set up access from your phone and other devices now? [Y/n] " -r answer < /dev/tty || answer=n
|
|
54
|
+
if [[ ! "$answer" =~ ^[Nn] ]]; then
|
|
55
|
+
exec "$INSTALL_DIR/scripts/agent-os" setup < /dev/tty
|
|
56
|
+
fi
|
|
57
|
+
fi
|
|
58
|
+
echo ""
|
|
59
|
+
echo "Next: agent-os setup"
|
package/scripts/lib/commands.sh
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Command implementations for agent-os
|
|
3
3
|
|
|
4
|
+
# The background service (launchd/systemd) the start/stop commands go through.
|
|
5
|
+
source "${BASH_SOURCE[0]%/*}/service.sh"
|
|
6
|
+
|
|
4
7
|
cmd_install() {
|
|
5
8
|
local use_local=false
|
|
6
9
|
[[ "${1:-}" == "--local" ]] && use_local=true
|
|
@@ -91,8 +94,8 @@ cmd_install() {
|
|
|
91
94
|
fi
|
|
92
95
|
|
|
93
96
|
echo "Next steps:"
|
|
94
|
-
echo " agent-os
|
|
95
|
-
echo " agent-os
|
|
97
|
+
echo " agent-os setup Start it, keep it running, and reach it from your phone"
|
|
98
|
+
echo " agent-os check Can other devices reach this machine? (names the cause if not)"
|
|
96
99
|
echo " agent-os status Show URLs"
|
|
97
100
|
wsl_next_steps
|
|
98
101
|
}
|
|
@@ -128,6 +131,16 @@ systemd_unit_active() {
|
|
|
128
131
|
}
|
|
129
132
|
|
|
130
133
|
cmd_start() {
|
|
134
|
+
if [[ "${OS:-}" == "macos" ]] && ! is_running && launchd_keeps_alive; then
|
|
135
|
+
if launchd_loaded; then
|
|
136
|
+
launchd_kickstart && log_success "AgentOS started (launchd)" && return 0
|
|
137
|
+
elif launchd_bootstrap; then
|
|
138
|
+
log_success "AgentOS started (launchd)"
|
|
139
|
+
return 0
|
|
140
|
+
fi
|
|
141
|
+
# No desktop session (ssh): run it by hand this time.
|
|
142
|
+
launchd_no_gui_session
|
|
143
|
+
fi
|
|
131
144
|
if ! is_running && has_systemd &&
|
|
132
145
|
systemctl --user is-enabled --quiet agent-os 2>/dev/null; then
|
|
133
146
|
systemctl --user start agent-os
|
|
@@ -186,6 +199,15 @@ cmd_start() {
|
|
|
186
199
|
}
|
|
187
200
|
|
|
188
201
|
cmd_stop() {
|
|
202
|
+
# A kill would just be restarted: launchd stops it, until the next login
|
|
203
|
+
# or 'agent-os start'.
|
|
204
|
+
if [[ "${OS:-}" == "macos" ]] && launchd_loaded && launchd_keeps_alive; then
|
|
205
|
+
log_info "Stopping AgentOS (launchd)..."
|
|
206
|
+
launchd_bootout
|
|
207
|
+
rm -f "$PID_FILE"
|
|
208
|
+
log_success "AgentOS stopped"
|
|
209
|
+
return 0
|
|
210
|
+
fi
|
|
189
211
|
if systemd_unit_active; then
|
|
190
212
|
log_info "Stopping AgentOS (systemd)..."
|
|
191
213
|
systemctl --user stop agent-os
|
|
@@ -226,6 +248,11 @@ cmd_stop() {
|
|
|
226
248
|
}
|
|
227
249
|
|
|
228
250
|
cmd_restart() {
|
|
251
|
+
if [[ "${OS:-}" == "macos" ]] && launchd_loaded && launchd_keeps_alive; then
|
|
252
|
+
launchd_restart
|
|
253
|
+
log_success "AgentOS restarted (launchd)"
|
|
254
|
+
return 0
|
|
255
|
+
fi
|
|
229
256
|
if systemd_unit_active; then
|
|
230
257
|
systemctl --user restart agent-os
|
|
231
258
|
log_success "AgentOS restarted (systemd)"
|
|
@@ -387,69 +414,34 @@ cmd_enable() {
|
|
|
387
414
|
exit 1
|
|
388
415
|
fi
|
|
389
416
|
|
|
390
|
-
local script_path
|
|
391
|
-
script_path=$(realpath "$0")
|
|
392
|
-
|
|
393
417
|
if [[ "$OS" == "macos" ]]; then
|
|
394
|
-
local
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
<string>$LOG_FILE</string>
|
|
416
|
-
<key>EnvironmentVariables</key>
|
|
417
|
-
<dict>
|
|
418
|
-
<key>PATH</key>
|
|
419
|
-
<string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
|
|
420
|
-
</dict>
|
|
421
|
-
</dict>
|
|
422
|
-
</plist>
|
|
423
|
-
EOF
|
|
424
|
-
|
|
425
|
-
launchctl load "$plist_path" 2>/dev/null || true
|
|
426
|
-
log_success "Auto-start enabled (launchd)"
|
|
427
|
-
echo " Plist: $plist_path"
|
|
418
|
+
local result path managed=false
|
|
419
|
+
path=$(launchd_plist_path)
|
|
420
|
+
launchd_loaded && launchd_keeps_alive && managed=true
|
|
421
|
+
if [[ "$(service_file_state)" == same && "$managed" == true ]]; then
|
|
422
|
+
log_success "Auto-start already enabled (launchd)"
|
|
423
|
+
echo " Plist: $path"
|
|
424
|
+
return 0
|
|
425
|
+
fi
|
|
426
|
+
# launchd runs the server itself from now on: one started by hand
|
|
427
|
+
# (or by the old plist) would hold the port it needs.
|
|
428
|
+
if is_running && [[ "$managed" == false ]]; then
|
|
429
|
+
cmd_stop
|
|
430
|
+
fi
|
|
431
|
+
result=$(service_reconcile)
|
|
432
|
+
[[ "$result" == updated ]] && log_info "Updated $path"
|
|
433
|
+
if launchd_bootstrap; then
|
|
434
|
+
log_success "Auto-start enabled (launchd): AgentOS starts at login and restarts after a crash"
|
|
435
|
+
else
|
|
436
|
+
launchd_no_gui_session
|
|
437
|
+
fi
|
|
438
|
+
echo " Plist: $path"
|
|
428
439
|
|
|
429
440
|
elif has_systemd; then
|
|
430
|
-
local
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
cat > "$service_path" << EOF
|
|
436
|
-
[Unit]
|
|
437
|
-
Description=AgentOS - AI Coding Session Manager
|
|
438
|
-
After=network.target
|
|
439
|
-
|
|
440
|
-
[Service]
|
|
441
|
-
Type=simple
|
|
442
|
-
ExecStart=$script_path start-foreground
|
|
443
|
-
Restart=on-failure
|
|
444
|
-
RestartSec=10
|
|
445
|
-
# Only the server is stopped on a restart: terminals (tmux), chat workers and
|
|
446
|
-
# an update in progress outlive it, as they do under launchd.
|
|
447
|
-
KillMode=process
|
|
448
|
-
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
|
449
|
-
|
|
450
|
-
[Install]
|
|
451
|
-
WantedBy=default.target
|
|
452
|
-
EOF
|
|
441
|
+
local service_path
|
|
442
|
+
service_path=$(systemd_unit_path)
|
|
443
|
+
# Writes the unit only when it differs; the rest is safe to repeat.
|
|
444
|
+
service_reconcile > /dev/null
|
|
453
445
|
|
|
454
446
|
systemctl --user daemon-reload
|
|
455
447
|
systemctl --user enable agent-os
|
|
@@ -486,8 +478,8 @@ cmd_disable() {
|
|
|
486
478
|
local plist_path="$HOME/Library/LaunchAgents/com.agent-os.plist"
|
|
487
479
|
|
|
488
480
|
if [[ -f "$plist_path" ]]; then
|
|
489
|
-
|
|
490
|
-
rm -f "$plist_path"
|
|
481
|
+
launchd_bootout
|
|
482
|
+
rm -f "$plist_path" "$PID_FILE"
|
|
491
483
|
log_success "Auto-start disabled"
|
|
492
484
|
else
|
|
493
485
|
log_warn "Auto-start was not enabled"
|
|
@@ -584,13 +576,15 @@ cmd_help() {
|
|
|
584
576
|
echo ""
|
|
585
577
|
echo "Commands:"
|
|
586
578
|
echo " install Install AgentOS (auto-installs dependencies)"
|
|
579
|
+
echo " setup Guided setup: background service, how devices reach it, phone"
|
|
580
|
+
echo " check Can other devices reach this machine? Names the cause and the fix"
|
|
587
581
|
echo " run Start server and open in browser"
|
|
588
582
|
echo " start Start the server in background"
|
|
589
583
|
echo " stop Stop the server"
|
|
590
584
|
echo " restart Restart the server"
|
|
591
585
|
echo " status Show server status and URLs"
|
|
592
586
|
echo " pair Add a phone, tablet or laptop (prints a QR code)"
|
|
593
|
-
echo " connect Turn on AgentOS Connect for this machine
|
|
587
|
+
echo " connect Turn on AgentOS Connect for this machine"
|
|
594
588
|
echo " disconnect Turn Connect off; the tunnel closes within seconds"
|
|
595
589
|
echo " link Link machines on your Connect account (link <name>, link accept <code>)"
|
|
596
590
|
echo " unlink Unlink a machine linked through Connect"
|