@saadnvd1/agent-os 0.3.765 → 0.3.768

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
@@ -50,7 +50,7 @@ and check on all of it from your phone.
50
50
 
51
51
  The iPhone app, [AgentOS: Coding Agents](https://apps.apple.com/us/app/agentos-coding-agents/id6820980797), is on the App Store.
52
52
 
53
- You need Node.js 22+, tmux, and at least one agent CLI (the installer offers to set up what's missing).
53
+ You need Node.js 22+, git, tmux, and at least one agent CLI (the installer offers to set up what's missing).
54
54
 
55
55
  ```bash
56
56
  npm install -g @saadnvd1/agent-os
@@ -69,11 +69,12 @@ Then open AgentOS, add a project from ⌘K, and press ⌥N. The [install guide](
69
69
 
70
70
  ## Connect
71
71
 
72
- Connect (in development) lets you reach AgentOS from anywhere, without a tailnet or opening a port. Sign in at [runagentos.com](https://runagentos.com), get a code, and run:
72
+ AgentOS Connect is live. It gives your machine a private address at runagentos.com, so your phone reaches it from any network with nothing to open on your router and no VPN.
73
73
 
74
- ```bash
75
- agent-os connect # links this machine with the code from runagentos.com
76
- ```
74
+ - **Free ($0):** Connect for one machine, including phone access from anywhere, on up to 3 devices.
75
+ - **Pro ($4/mo billed yearly, or $6 monthly):** link up to 10 machines, with unlimited devices and 100 GB of traffic a month.
76
+
77
+ [Sign up](https://runagentos.com/sign-in?mode=sign-up), then run `agent-os connect` on the machine and enter the code. The [phone guide](docs/guides/phone.md) has the steps.
77
78
 
78
79
  Reach it from your phone with the [AgentOS iPhone app](https://apps.apple.com/us/app/agentos-coding-agents/id6820980797), now on the App Store.
79
80
 
@@ -1,4 +1,8 @@
1
1
  import { Info } from "lucide-react";
2
+ import { Button } from "@/components/ui/button";
3
+ import { CONNECT_SIGN_UP } from "@/lib/connect/view";
4
+
5
+ const INSTALL_URL = "https://github.com/saadnvd1/agent-os#install";
2
6
 
3
7
  // Where a terminal or a new session would be, in a demo: what this is and
4
8
  // what to look at instead. Fixed text; nothing runs.
@@ -18,6 +22,18 @@ export function DemoNotice() {
18
22
  Open a session from the sidebar to read its chat, its changes and its
19
23
  pull request; its terminal stays closed.
20
24
  </p>
25
+ <div className="flex flex-wrap gap-2 pt-1">
26
+ <Button asChild className="min-h-11 sm:min-h-9">
27
+ <a href={INSTALL_URL} target="_blank" rel="noopener noreferrer">
28
+ Install AgentOS
29
+ </a>
30
+ </Button>
31
+ <Button asChild variant="outline" className="min-h-11 sm:min-h-9">
32
+ <a href={CONNECT_SIGN_UP} target="_blank" rel="noopener noreferrer">
33
+ Get Connect
34
+ </a>
35
+ </Button>
36
+ </div>
21
37
  </div>
22
38
  </div>
23
39
  );
@@ -3,7 +3,11 @@
3
3
  import { Cloud } from "lucide-react";
4
4
  import { Switch } from "@/components/ui/switch";
5
5
  import type { ConnectStatus } from "@/lib/connect/serve";
6
- import { CONNECT_STATE as STATE, connectView } from "@/lib/connect/view";
6
+ import {
7
+ CONNECT_SIGN_UP,
8
+ CONNECT_STATE as STATE,
9
+ connectView,
10
+ } from "@/lib/connect/view";
7
11
 
8
12
  export function ConnectCard({
9
13
  connect,
@@ -38,7 +42,21 @@ export function ConnectCard({
38
42
  ) : enrolled ? (
39
43
  <>Linked. The address appears once the tunnel starts.</>
40
44
  ) : (
41
- <>Not set up on this machine. Run agent-os connect in a terminal.</>
45
+ <>
46
+ Not set up on this machine. Run{" "}
47
+ <code className="text-foreground font-mono">
48
+ agent-os connect
49
+ </code>{" "}
50
+ in a terminal.{" "}
51
+ <a
52
+ href={CONNECT_SIGN_UP}
53
+ target="_blank"
54
+ rel="noopener noreferrer"
55
+ className="text-foreground flex min-h-11 w-fit items-center underline underline-offset-2 sm:min-h-6"
56
+ >
57
+ Set up Connect (free)
58
+ </a>
59
+ </>
42
60
  )}
43
61
  {connect.reason && connect.state !== "off"
44
62
  ? ` ${connect.reason.replace(/\.$/, "")}.`
@@ -73,7 +73,7 @@ export function ProviderCard({
73
73
  const builtIn = provider.id === ANTHROPIC;
74
74
  const [open, setOpen] = useState(false);
75
75
  const [kind, setKind] = useState<SecretRef["kind"]>(
76
- provider.auth?.kind ?? "tokenvault"
76
+ provider.auth?.kind ?? "env"
77
77
  );
78
78
  const [a0, b0] = refFields(provider.auth);
79
79
  const [a, setA] = useState(a0);
@@ -3,6 +3,7 @@
3
3
  import { AlertCircle, FolderPlus, Plus, X } from "lucide-react";
4
4
  import { Button } from "@/components/ui/button";
5
5
  import { ShimmeringLoader } from "@/components/ui/skeleton";
6
+ import { chordHint } from "@/lib/keybindings/chord";
6
7
  import { sidebarUiActions } from "@/stores/sidebarUi";
7
8
  import type { SidebarData } from "./useSidebarData";
8
9
  import { MachineShelves, SessionShelves } from "./SessionShelves";
@@ -99,10 +100,19 @@ export function SidebarBody({
99
100
  <Empty
100
101
  text="Create a project to start your first session"
101
102
  action={
102
- <Button onClick={onNewProject} className="h-11 gap-2 md:h-9">
103
- <FolderPlus className="h-4 w-4" />
104
- Add project
105
- </Button>
103
+ <div className="flex flex-col items-center gap-3">
104
+ <Button onClick={onNewProject} className="h-11 gap-2 md:h-9">
105
+ <FolderPlus className="h-4 w-4" />
106
+ Add project
107
+ </Button>
108
+ <p className="text-muted-foreground/70 text-xs">
109
+ Then press{" "}
110
+ <kbd className="bg-muted text-foreground rounded px-1.5 py-0.5 font-mono">
111
+ {chordHint("alt+n")}
112
+ </kbd>{" "}
113
+ to start a session
114
+ </p>
115
+ </div>
106
116
  }
107
117
  />
108
118
  ) : (
@@ -13,7 +13,11 @@ import {
13
13
  useNetworkQuery,
14
14
  useUpdateNetwork,
15
15
  } from "@/data/devices";
16
- import { CONNECT_SITE, CONNECT_STATE, connectView } from "@/lib/connect/view";
16
+ import {
17
+ CONNECT_SIGN_UP,
18
+ CONNECT_STATE,
19
+ connectView,
20
+ } from "@/lib/connect/view";
17
21
  import { Block } from "./Block";
18
22
 
19
23
  const COMMAND = "agent-os connect";
@@ -40,7 +44,7 @@ export function LinkForm() {
40
44
  <p className="text-muted-foreground text-xs">
41
45
  Get a code at{" "}
42
46
  <a
43
- href={CONNECT_SITE}
47
+ href={CONNECT_SIGN_UP}
44
48
  target="_blank"
45
49
  rel="noopener noreferrer"
46
50
  className="text-foreground inline-flex items-center gap-1 underline underline-offset-2"
@@ -48,7 +52,7 @@ export function LinkForm() {
48
52
  runagentos.com
49
53
  <ExternalLink className="size-3" />
50
54
  </a>{" "}
51
- (Connect a machine), then paste it here.
55
+ (free account, then Connect a machine), then paste it here.
52
56
  </p>
53
57
  <form
54
58
  className="flex gap-2"
@@ -104,7 +108,7 @@ export function ConnectBlock() {
104
108
  <Block
105
109
  icon={Globe}
106
110
  title="Use it away from home"
107
- detail="Connect gives this machine a private address at runagentos.com, so your phone reaches it from anywhere. Nothing to open on your router."
111
+ detail="Connect is free for one machine. It gives this machine a private address at runagentos.com, so your phone reaches it from anywhere. Nothing to open on your router."
108
112
  >
109
113
  {network.isPending ? (
110
114
  <div className="bg-muted/40 h-12 animate-pulse rounded-lg" />
@@ -28,7 +28,7 @@ const STEPS: Record<
28
28
  agents: {
29
29
  label: "Agents",
30
30
  title: "Connect your agents",
31
- hint: "Install and sign in the coding agents you want to use. You need one.",
31
+ hint: "Install and sign in the coding agents you want to use. You need one, with an account such as Claude or Codex.",
32
32
  },
33
33
  project: {
34
34
  label: "Project",
@@ -1,6 +1,7 @@
1
1
  import type { ConnectStatus } from "./serve";
2
2
 
3
3
  export const CONNECT_SITE = "https://runagentos.com";
4
+ export const CONNECT_SIGN_UP = `${CONNECT_SITE}/sign-in?mode=sign-up`;
4
5
 
5
6
  export const CONNECT_STATE: Record<ConnectStatus["state"], string> = {
6
7
  up: "Connected",
@@ -25,7 +25,7 @@ import { getProject } from "../../projects";
25
25
  import { planFor, splitOf } from "../../plans/features";
26
26
  import { moveTask } from "../move-flow";
27
27
  import * as client from "./client";
28
- import { meetsNeeds, needsOf } from "../placement";
28
+ import { meetsNeeds, needsOf, taskPinnedToMac } from "../placement";
29
29
  import { remotePlanPath } from "./plan-path";
30
30
  import { DEFAULT_PRIORITY, isPriority } from "../schedule-types";
31
31
  import { localHolds } from "../hold-checks";
@@ -55,6 +55,9 @@ function checkedSplit(s: Session, text: string | null) {
55
55
  }
56
56
  }
57
57
 
58
+ const pinned = (s: Session, project: { working_directory: string }) =>
59
+ taskPinnedToMac(s.id, expandHome(project.working_directory));
60
+
58
61
  /**
59
62
  * Where a written plan is built: "there" (approved on the machine that
60
63
  * planned it) or "home" (moved here first). Pieces are checked against this
@@ -70,6 +73,9 @@ export function implementTarget(
70
73
  // The project lives there: nothing here to build it in.
71
74
  if (!project || isRemoteHost(project.host_id)) return "there";
72
75
  if (split) return "home";
76
+ // An explicit Mac pin builds on the Mac, wherever the plan ran.
77
+ if (pinned(s, project))
78
+ return meetsNeeds(["mac"], machineCapabilities()) ? "home" : "there";
73
79
  const { needs } = needsOf({
74
80
  projectId: project.id,
75
81
  projectPath: expandHome(project.working_directory),
@@ -229,6 +235,7 @@ export async function handoffHome(
229
235
  projectPath: expandHome(project.working_directory),
230
236
  prompt: s.task_prompt ?? "",
231
237
  });
238
+ if (pinned(s, project) && !needs.includes("mac")) needs.push("mac");
232
239
  const holds = localHolds(
233
240
  {
234
241
  needs,
@@ -79,6 +79,28 @@ export function inferNeeds(projectPath: string): Need[] {
79
79
  return [];
80
80
  }
81
81
 
82
+ // Whether the starter asked for a Mac outright: the request's `machine`
83
+ // (stored on the queue row, which shares the task's id), a recipe's, or the
84
+ // project's own machine setting. A need read from the brief or the files
85
+ // doesn't count: that one a plan can ignore.
86
+ export function pinnedToMac(
87
+ stored: string | null,
88
+ projectPath: string
89
+ ): boolean {
90
+ const asked = parseNeeds(stored);
91
+ if (asked) return asked.includes("mac");
92
+ return !!loadProjectConfig(projectPath).config.machine?.needs?.includes(
93
+ "mac"
94
+ );
95
+ }
96
+
97
+ export function taskPinnedToMac(taskId: string, projectPath: string): boolean {
98
+ const row = db
99
+ .prepare(`SELECT machine_needs FROM task_queue WHERE id = ?`)
100
+ .get(taskId) as { machine_needs: string | null } | undefined;
101
+ return pinnedToMac(row?.machine_needs ?? null, projectPath);
102
+ }
103
+
82
104
  export function needsOf(input: {
83
105
  projectId: string;
84
106
  projectPath: string;
@@ -13,6 +13,7 @@ import { usesGitCrypt } from "../worktree-add";
13
13
  import { list as hostTasks } from "./contract/client";
14
14
  import {
15
15
  asPlanPhase,
16
+ pinnedToMac,
16
17
  place,
17
18
  unplaceable,
18
19
  wantsPlacing,
@@ -90,11 +91,22 @@ async function peerOptions(
90
91
  return out;
91
92
  }
92
93
 
93
- // A start that plans first goes by its plan phase: it needs no Mac or
94
- // simulator, and a plan-only one that no machine can take starts here
94
+ // A start that plans first goes by its plan phase: it needs no simulator, and a plan-only one that no machine can take starts here
95
95
  // as ever (it never waits on load).
96
96
  const plansFirst = (r: QueueRow) => !!(r.plan || r.plan_only);
97
97
 
98
+ // Planning needs no simulator or Xcode, and a need read from the files or the
99
+ // brief doesn't hold it. Only an explicit Mac pin does: the work is Mac-only
100
+ // (often because the other machine can't reach what it touches).
101
+ const planNeeds = (r: QueueRow, needs: Need[]): Need[] => {
102
+ const project = getProject(r.project_id);
103
+ return project &&
104
+ needs.includes("mac") &&
105
+ pinnedToMac(r.machine_needs, expandHome(project.working_directory))
106
+ ? ["mac"]
107
+ : [];
108
+ };
109
+
98
110
  // Marks each start the home machine's load rules out as remote and returns
99
111
  // where it goes. "stale": the line moved while the machines were asked, so
100
112
  // this look is over, and the one a change during it asks for sees the line
@@ -143,7 +155,7 @@ export async function placeCandidates(
143
155
  asked(c),
144
156
  home,
145
157
  options,
146
- plan ? [] : (needsOf.get(c.id) ?? []),
158
+ plan ? planNeeds(r, needsOf.get(c.id) ?? []) : (needsOf.get(c.id) ?? []),
147
159
  now
148
160
  );
149
161
  if (!spot) continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saadnvd1/agent-os",
3
- "version": "0.3.765",
3
+ "version": "0.3.768",
4
4
  "description": "Self-hosted web UI for managing Claude Code sessions",
5
5
  "bin": {
6
6
  "agent-os": "scripts/agent-os"