@addai/node 0.7.0 → 0.8.1
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 +96 -96
- package/dist/autostart-mac.js +30 -30
- package/dist/autostart-win.js +50 -50
- package/dist/cli.js +17 -17
- package/dist/session-runner.js +15 -0
- package/dist/tui/dashboard.d.ts +21 -0
- package/dist/tui/dashboard.js +86 -8
- package/dist/tui/run.js +1 -1
- package/package.json +60 -60
- package/scripts/fix-pty-helper.js +28 -28
- package/scripts/precompact-capture.js +292 -292
- package/scripts/probe-tui.mjs +122 -122
- package/scripts/smoke-test.sh +74 -74
package/README.md
CHANGED
|
@@ -1,96 +1,96 @@
|
|
|
1
|
-
# entities-runtime
|
|
2
|
-
|
|
3
|
-
A standalone Node.js daemon. Install it on any machine, pair with your
|
|
4
|
-
+Ai account, and your account can then run Claude Code / Codex sessions
|
|
5
|
-
on that machine on its behalf — from Vault, from Entity Studio, or from
|
|
6
|
-
anywhere else in +Ai.
|
|
7
|
-
|
|
8
|
-
## Quick start
|
|
9
|
-
|
|
10
|
-
```bash
|
|
11
|
-
npx entities-runtime
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
The first run prints a 5-letter pairing code and a URL:
|
|
15
|
-
|
|
16
|
-
```
|
|
17
|
-
Pair this runtime with your +Ai account.
|
|
18
|
-
|
|
19
|
-
https://vault.add.ai/entity/connect/HDXYC
|
|
20
|
-
|
|
21
|
-
Enter this code: HDXYC
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
Open the URL while signed into Vault, enter the code, and the runtime
|
|
25
|
-
starts polling for work. Capabilities (claude, codex, git, node) are
|
|
26
|
-
reported every 30 s.
|
|
27
|
-
|
|
28
|
-
## Commands
|
|
29
|
-
|
|
30
|
-
```
|
|
31
|
-
entities-runtime run the daemon (foreground)
|
|
32
|
-
entities-runtime status pairing + capabilities + last-seen
|
|
33
|
-
entities-runtime sessions recent requests handled (--limit N)
|
|
34
|
-
entities-runtime unpair --yes disconnect from +Ai
|
|
35
|
-
entities-runtime version print version
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
`status` and `sessions` work without the daemon running — they hit
|
|
39
|
-
Supabase directly using the daemon token in
|
|
40
|
-
`~/.entities-runtime/state.json`.
|
|
41
|
-
|
|
42
|
-
## Architecture
|
|
43
|
-
|
|
44
|
-
See [`docs/architecture.md`](docs/architecture.md) for the full picture:
|
|
45
|
-
Supabase tables, RPC inventory, request lifecycle, and the cancel-watcher
|
|
46
|
-
flow.
|
|
47
|
-
|
|
48
|
-
## DiskGuard — Codex scratch-dir disk protection
|
|
49
|
-
|
|
50
|
-
Each Codex run gets a throwaway `CODEX_HOME` under the OS temp dir
|
|
51
|
-
(`entities-codex-<rand>`). Codex writes its `state_*.sqlite`, logs, caches
|
|
52
|
-
and shell snapshots there — typically 50–350 MB per run. codex-spawn now
|
|
53
|
-
deletes that dir on exit, but a `SIGKILL`, a crash, or an orphan left by a
|
|
54
|
-
previous runtime process can skip that path. On a dedicated host doing
|
|
55
|
-
thousands of runs, those leftovers once grew to ~158 GiB and filled the
|
|
56
|
-
disk (`ENOSPC`, repeated crashes).
|
|
57
|
-
|
|
58
|
-
**DiskGuard** is the safety net. Like a CCTV DVR overwriting the oldest
|
|
59
|
-
footage when the disk fills, it watches free space on the volume holding
|
|
60
|
-
the temp dir and, when usage crosses a high-water mark, deletes the
|
|
61
|
-
**oldest inactive** scratch dirs — oldest first — until usage drops back
|
|
62
|
-
below a low-water mark, leaving a buffer for the OS. It runs at startup, on
|
|
63
|
-
an interval, and opportunistically right before each new Codex run.
|
|
64
|
-
|
|
65
|
-
Safety guarantees: it only touches directories directly under the temp root
|
|
66
|
-
whose names match the configured prefix (realpath-validated); it never
|
|
67
|
-
deletes a dir owned by a live session (in-process, or one whose
|
|
68
|
-
`session.lock` PID is still alive) or one younger than the min-age guard;
|
|
69
|
-
and it removes symlink *entries* without following them, so the durable
|
|
70
|
-
`~/.codex` targets behind `sessions`/`auth.json` are never harmed.
|
|
71
|
-
|
|
72
|
-
Note: DiskGuard can only reclaim space by deleting scratch dirs it owns. If
|
|
73
|
-
a disk is full of *other* data it will evict what it can and log that it
|
|
74
|
-
couldn't reach the target rather than touching anything it doesn't own.
|
|
75
|
-
|
|
76
|
-
Configure via environment variables (defaults shown):
|
|
77
|
-
|
|
78
|
-
| Variable | Default | Meaning |
|
|
79
|
-
|---|---|---|
|
|
80
|
-
| `DISKGUARD_ENABLED` | `true` | Master switch for the sweeper. Cleanup-on-exit still runs when `false`. |
|
|
81
|
-
| `DISKGUARD_HIGH_WATER_PCT` | `90` | Start evicting when volume usage reaches this %. |
|
|
82
|
-
| `DISKGUARD_LOW_WATER_PCT` | `80` | Stop evicting once usage drops below this %. Clamped below high-water. |
|
|
83
|
-
| `DISKGUARD_MIN_AGE_MINUTES` | `30` | Never evict a scratch dir younger than this. |
|
|
84
|
-
| `DISKGUARD_CHECK_INTERVAL_SECONDS` | `60` | Periodic sweep cadence. |
|
|
85
|
-
| `DISKGUARD_DRY_RUN` | `false` | Log what would be evicted without deleting anything. |
|
|
86
|
-
| `DISKGUARD_PREFIXES` | `entities-codex-` | Comma-separated dir-name prefixes eligible for eviction. |
|
|
87
|
-
| `DISKGUARD_TEMP_ROOT` | `os.tmpdir()` | Override the watched temp root (mainly for testing). |
|
|
88
|
-
|
|
89
|
-
## Development
|
|
90
|
-
|
|
91
|
-
```bash
|
|
92
|
-
npm install
|
|
93
|
-
npm run build
|
|
94
|
-
node dist/cli.js
|
|
95
|
-
npm test # builds, then runs the node:test suite (see test/)
|
|
96
|
-
```
|
|
1
|
+
# entities-runtime
|
|
2
|
+
|
|
3
|
+
A standalone Node.js daemon. Install it on any machine, pair with your
|
|
4
|
+
+Ai account, and your account can then run Claude Code / Codex sessions
|
|
5
|
+
on that machine on its behalf — from Vault, from Entity Studio, or from
|
|
6
|
+
anywhere else in +Ai.
|
|
7
|
+
|
|
8
|
+
## Quick start
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npx entities-runtime
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The first run prints a 5-letter pairing code and a URL:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
Pair this runtime with your +Ai account.
|
|
18
|
+
|
|
19
|
+
https://vault.add.ai/entity/connect/HDXYC
|
|
20
|
+
|
|
21
|
+
Enter this code: HDXYC
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Open the URL while signed into Vault, enter the code, and the runtime
|
|
25
|
+
starts polling for work. Capabilities (claude, codex, git, node) are
|
|
26
|
+
reported every 30 s.
|
|
27
|
+
|
|
28
|
+
## Commands
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
entities-runtime run the daemon (foreground)
|
|
32
|
+
entities-runtime status pairing + capabilities + last-seen
|
|
33
|
+
entities-runtime sessions recent requests handled (--limit N)
|
|
34
|
+
entities-runtime unpair --yes disconnect from +Ai
|
|
35
|
+
entities-runtime version print version
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`status` and `sessions` work without the daemon running — they hit
|
|
39
|
+
Supabase directly using the daemon token in
|
|
40
|
+
`~/.entities-runtime/state.json`.
|
|
41
|
+
|
|
42
|
+
## Architecture
|
|
43
|
+
|
|
44
|
+
See [`docs/architecture.md`](docs/architecture.md) for the full picture:
|
|
45
|
+
Supabase tables, RPC inventory, request lifecycle, and the cancel-watcher
|
|
46
|
+
flow.
|
|
47
|
+
|
|
48
|
+
## DiskGuard — Codex scratch-dir disk protection
|
|
49
|
+
|
|
50
|
+
Each Codex run gets a throwaway `CODEX_HOME` under the OS temp dir
|
|
51
|
+
(`entities-codex-<rand>`). Codex writes its `state_*.sqlite`, logs, caches
|
|
52
|
+
and shell snapshots there — typically 50–350 MB per run. codex-spawn now
|
|
53
|
+
deletes that dir on exit, but a `SIGKILL`, a crash, or an orphan left by a
|
|
54
|
+
previous runtime process can skip that path. On a dedicated host doing
|
|
55
|
+
thousands of runs, those leftovers once grew to ~158 GiB and filled the
|
|
56
|
+
disk (`ENOSPC`, repeated crashes).
|
|
57
|
+
|
|
58
|
+
**DiskGuard** is the safety net. Like a CCTV DVR overwriting the oldest
|
|
59
|
+
footage when the disk fills, it watches free space on the volume holding
|
|
60
|
+
the temp dir and, when usage crosses a high-water mark, deletes the
|
|
61
|
+
**oldest inactive** scratch dirs — oldest first — until usage drops back
|
|
62
|
+
below a low-water mark, leaving a buffer for the OS. It runs at startup, on
|
|
63
|
+
an interval, and opportunistically right before each new Codex run.
|
|
64
|
+
|
|
65
|
+
Safety guarantees: it only touches directories directly under the temp root
|
|
66
|
+
whose names match the configured prefix (realpath-validated); it never
|
|
67
|
+
deletes a dir owned by a live session (in-process, or one whose
|
|
68
|
+
`session.lock` PID is still alive) or one younger than the min-age guard;
|
|
69
|
+
and it removes symlink *entries* without following them, so the durable
|
|
70
|
+
`~/.codex` targets behind `sessions`/`auth.json` are never harmed.
|
|
71
|
+
|
|
72
|
+
Note: DiskGuard can only reclaim space by deleting scratch dirs it owns. If
|
|
73
|
+
a disk is full of *other* data it will evict what it can and log that it
|
|
74
|
+
couldn't reach the target rather than touching anything it doesn't own.
|
|
75
|
+
|
|
76
|
+
Configure via environment variables (defaults shown):
|
|
77
|
+
|
|
78
|
+
| Variable | Default | Meaning |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `DISKGUARD_ENABLED` | `true` | Master switch for the sweeper. Cleanup-on-exit still runs when `false`. |
|
|
81
|
+
| `DISKGUARD_HIGH_WATER_PCT` | `90` | Start evicting when volume usage reaches this %. |
|
|
82
|
+
| `DISKGUARD_LOW_WATER_PCT` | `80` | Stop evicting once usage drops below this %. Clamped below high-water. |
|
|
83
|
+
| `DISKGUARD_MIN_AGE_MINUTES` | `30` | Never evict a scratch dir younger than this. |
|
|
84
|
+
| `DISKGUARD_CHECK_INTERVAL_SECONDS` | `60` | Periodic sweep cadence. |
|
|
85
|
+
| `DISKGUARD_DRY_RUN` | `false` | Log what would be evicted without deleting anything. |
|
|
86
|
+
| `DISKGUARD_PREFIXES` | `entities-codex-` | Comma-separated dir-name prefixes eligible for eviction. |
|
|
87
|
+
| `DISKGUARD_TEMP_ROOT` | `os.tmpdir()` | Override the watched temp root (mainly for testing). |
|
|
88
|
+
|
|
89
|
+
## Development
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npm install
|
|
93
|
+
npm run build
|
|
94
|
+
node dist/cli.js
|
|
95
|
+
npm test # builds, then runs the node:test suite (see test/)
|
|
96
|
+
```
|
package/dist/autostart-mac.js
CHANGED
|
@@ -79,36 +79,36 @@ function buildPlist(spec) {
|
|
|
79
79
|
const env = Object.entries(spec.env)
|
|
80
80
|
.map(([k, v]) => ` <key>${esc(k)}</key>\n <string>${esc(v)}</string>`)
|
|
81
81
|
.join('\n');
|
|
82
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
83
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
84
|
-
<plist version="1.0">
|
|
85
|
-
<dict>
|
|
86
|
-
<key>Label</key>
|
|
87
|
-
<string>${esc(spec.label)}</string>
|
|
88
|
-
<key>ProgramArguments</key>
|
|
89
|
-
<array>
|
|
90
|
-
${args}
|
|
91
|
-
</array>
|
|
92
|
-
<key>RunAtLoad</key>
|
|
93
|
-
<true/>
|
|
94
|
-
<key>KeepAlive</key>
|
|
95
|
-
<true/>
|
|
96
|
-
<key>ThrottleInterval</key>
|
|
97
|
-
<integer>10</integer>
|
|
98
|
-
<key>ProcessType</key>
|
|
99
|
-
<string>Interactive</string>
|
|
100
|
-
<key>WorkingDirectory</key>
|
|
101
|
-
<string>${esc(spec.workingDirectory)}</string>
|
|
102
|
-
<key>StandardOutPath</key>
|
|
103
|
-
<string>${esc(spec.logPath)}</string>
|
|
104
|
-
<key>StandardErrorPath</key>
|
|
105
|
-
<string>${esc(spec.logPath)}</string>
|
|
106
|
-
<key>EnvironmentVariables</key>
|
|
107
|
-
<dict>
|
|
108
|
-
${env}
|
|
109
|
-
</dict>
|
|
110
|
-
</dict>
|
|
111
|
-
</plist>
|
|
82
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
83
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
84
|
+
<plist version="1.0">
|
|
85
|
+
<dict>
|
|
86
|
+
<key>Label</key>
|
|
87
|
+
<string>${esc(spec.label)}</string>
|
|
88
|
+
<key>ProgramArguments</key>
|
|
89
|
+
<array>
|
|
90
|
+
${args}
|
|
91
|
+
</array>
|
|
92
|
+
<key>RunAtLoad</key>
|
|
93
|
+
<true/>
|
|
94
|
+
<key>KeepAlive</key>
|
|
95
|
+
<true/>
|
|
96
|
+
<key>ThrottleInterval</key>
|
|
97
|
+
<integer>10</integer>
|
|
98
|
+
<key>ProcessType</key>
|
|
99
|
+
<string>Interactive</string>
|
|
100
|
+
<key>WorkingDirectory</key>
|
|
101
|
+
<string>${esc(spec.workingDirectory)}</string>
|
|
102
|
+
<key>StandardOutPath</key>
|
|
103
|
+
<string>${esc(spec.logPath)}</string>
|
|
104
|
+
<key>StandardErrorPath</key>
|
|
105
|
+
<string>${esc(spec.logPath)}</string>
|
|
106
|
+
<key>EnvironmentVariables</key>
|
|
107
|
+
<dict>
|
|
108
|
+
${env}
|
|
109
|
+
</dict>
|
|
110
|
+
</dict>
|
|
111
|
+
</plist>
|
|
112
112
|
`;
|
|
113
113
|
}
|
|
114
114
|
/** Pull ProgramArguments back out of a plist we wrote, for `status`. */
|
package/dist/autostart-win.js
CHANGED
|
@@ -128,56 +128,56 @@ function buildVbs(cmdPath) {
|
|
|
128
128
|
* a second daemon.
|
|
129
129
|
*/
|
|
130
130
|
function buildTaskXml(spec) {
|
|
131
|
-
return `<?xml version="1.0" encoding="UTF-16"?>
|
|
132
|
-
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
133
|
-
<RegistrationInfo>
|
|
134
|
-
<Date>${xmlEsc(spec.createdAt)}</Date>
|
|
135
|
-
<Author>@addai/node</Author>
|
|
136
|
-
<Description>Runs this +Ai Node so entities can reach it. Created by \`ainode startup enable\`.</Description>
|
|
137
|
-
</RegistrationInfo>
|
|
138
|
-
<Triggers>
|
|
139
|
-
<LogonTrigger>
|
|
140
|
-
<Enabled>true</Enabled>
|
|
141
|
-
<UserId>${xmlEsc(spec.userId)}</UserId>
|
|
142
|
-
<Repetition>
|
|
143
|
-
<Interval>PT5M</Interval>
|
|
144
|
-
<StopAtDurationEnd>false</StopAtDurationEnd>
|
|
145
|
-
</Repetition>
|
|
146
|
-
</LogonTrigger>
|
|
147
|
-
</Triggers>
|
|
148
|
-
<Principals>
|
|
149
|
-
<Principal id="Author">
|
|
150
|
-
<UserId>${xmlEsc(spec.userId)}</UserId>
|
|
151
|
-
<LogonType>InteractiveToken</LogonType>
|
|
152
|
-
<RunLevel>LeastPrivilege</RunLevel>
|
|
153
|
-
</Principal>
|
|
154
|
-
</Principals>
|
|
155
|
-
<Settings>
|
|
156
|
-
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
|
157
|
-
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
|
158
|
-
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
|
159
|
-
<AllowHardTerminate>true</AllowHardTerminate>
|
|
160
|
-
<StartWhenAvailable>true</StartWhenAvailable>
|
|
161
|
-
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
|
|
162
|
-
<IdleSettings>
|
|
163
|
-
<StopOnIdleEnd>false</StopOnIdleEnd>
|
|
164
|
-
<RestartOnIdle>false</RestartOnIdle>
|
|
165
|
-
</IdleSettings>
|
|
166
|
-
<AllowStartOnDemand>true</AllowStartOnDemand>
|
|
167
|
-
<Enabled>true</Enabled>
|
|
168
|
-
<Hidden>false</Hidden>
|
|
169
|
-
<RunOnlyIfIdle>false</RunOnlyIfIdle>
|
|
170
|
-
<WakeToRun>false</WakeToRun>
|
|
171
|
-
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
|
|
172
|
-
<Priority>7</Priority>
|
|
173
|
-
</Settings>
|
|
174
|
-
<Actions Context="Author">
|
|
175
|
-
<Exec>
|
|
176
|
-
<Command>${xmlEsc(spec.wscriptPath)}</Command>
|
|
177
|
-
<Arguments>"${xmlEsc(spec.vbsPath)}"</Arguments>
|
|
178
|
-
</Exec>
|
|
179
|
-
</Actions>
|
|
180
|
-
</Task>
|
|
131
|
+
return `<?xml version="1.0" encoding="UTF-16"?>
|
|
132
|
+
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
133
|
+
<RegistrationInfo>
|
|
134
|
+
<Date>${xmlEsc(spec.createdAt)}</Date>
|
|
135
|
+
<Author>@addai/node</Author>
|
|
136
|
+
<Description>Runs this +Ai Node so entities can reach it. Created by \`ainode startup enable\`.</Description>
|
|
137
|
+
</RegistrationInfo>
|
|
138
|
+
<Triggers>
|
|
139
|
+
<LogonTrigger>
|
|
140
|
+
<Enabled>true</Enabled>
|
|
141
|
+
<UserId>${xmlEsc(spec.userId)}</UserId>
|
|
142
|
+
<Repetition>
|
|
143
|
+
<Interval>PT5M</Interval>
|
|
144
|
+
<StopAtDurationEnd>false</StopAtDurationEnd>
|
|
145
|
+
</Repetition>
|
|
146
|
+
</LogonTrigger>
|
|
147
|
+
</Triggers>
|
|
148
|
+
<Principals>
|
|
149
|
+
<Principal id="Author">
|
|
150
|
+
<UserId>${xmlEsc(spec.userId)}</UserId>
|
|
151
|
+
<LogonType>InteractiveToken</LogonType>
|
|
152
|
+
<RunLevel>LeastPrivilege</RunLevel>
|
|
153
|
+
</Principal>
|
|
154
|
+
</Principals>
|
|
155
|
+
<Settings>
|
|
156
|
+
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
|
157
|
+
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
|
158
|
+
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
|
159
|
+
<AllowHardTerminate>true</AllowHardTerminate>
|
|
160
|
+
<StartWhenAvailable>true</StartWhenAvailable>
|
|
161
|
+
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
|
|
162
|
+
<IdleSettings>
|
|
163
|
+
<StopOnIdleEnd>false</StopOnIdleEnd>
|
|
164
|
+
<RestartOnIdle>false</RestartOnIdle>
|
|
165
|
+
</IdleSettings>
|
|
166
|
+
<AllowStartOnDemand>true</AllowStartOnDemand>
|
|
167
|
+
<Enabled>true</Enabled>
|
|
168
|
+
<Hidden>false</Hidden>
|
|
169
|
+
<RunOnlyIfIdle>false</RunOnlyIfIdle>
|
|
170
|
+
<WakeToRun>false</WakeToRun>
|
|
171
|
+
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
|
|
172
|
+
<Priority>7</Priority>
|
|
173
|
+
</Settings>
|
|
174
|
+
<Actions Context="Author">
|
|
175
|
+
<Exec>
|
|
176
|
+
<Command>${xmlEsc(spec.wscriptPath)}</Command>
|
|
177
|
+
<Arguments>"${xmlEsc(spec.vbsPath)}"</Arguments>
|
|
178
|
+
</Exec>
|
|
179
|
+
</Actions>
|
|
180
|
+
</Task>
|
|
181
181
|
`;
|
|
182
182
|
}
|
|
183
183
|
function schtasks(args) {
|
package/dist/cli.js
CHANGED
|
@@ -192,23 +192,23 @@ async function main() {
|
|
|
192
192
|
case 'help':
|
|
193
193
|
case '--help':
|
|
194
194
|
case '-h':
|
|
195
|
-
console.log(`ainode v${index_1.VERSION}
|
|
196
|
-
|
|
197
|
-
usage:
|
|
198
|
-
ainode run this +Ai Node — daemon + live dashboard
|
|
199
|
-
ainode harnesses install / log in agent harnesses (interactive)
|
|
200
|
-
ainode startup enable start this node whenever you log in
|
|
201
|
-
ainode startup disable stop starting at login
|
|
202
|
-
ainode startup status where the startup entry is and what it runs
|
|
203
|
-
ainode unpair --yes disconnect from +Ai
|
|
204
|
-
ainode version print version
|
|
205
|
-
|
|
206
|
-
Piped or headless output falls back to plain log lines. Set AINODE_NO_TUI=1
|
|
207
|
-
to force that on a terminal. \`ainode run --ensure\` starts a node only if one
|
|
208
|
-
isn't already running — what the Windows startup task uses.
|
|
209
|
-
|
|
210
|
-
Pair the node by running it with no args and following the prompt
|
|
211
|
-
to vault.add.ai/entity/connect/<CODE>.
|
|
195
|
+
console.log(`ainode v${index_1.VERSION}
|
|
196
|
+
|
|
197
|
+
usage:
|
|
198
|
+
ainode run this +Ai Node — daemon + live dashboard
|
|
199
|
+
ainode harnesses install / log in agent harnesses (interactive)
|
|
200
|
+
ainode startup enable start this node whenever you log in
|
|
201
|
+
ainode startup disable stop starting at login
|
|
202
|
+
ainode startup status where the startup entry is and what it runs
|
|
203
|
+
ainode unpair --yes disconnect from +Ai
|
|
204
|
+
ainode version print version
|
|
205
|
+
|
|
206
|
+
Piped or headless output falls back to plain log lines. Set AINODE_NO_TUI=1
|
|
207
|
+
to force that on a terminal. \`ainode run --ensure\` starts a node only if one
|
|
208
|
+
isn't already running — what the Windows startup task uses.
|
|
209
|
+
|
|
210
|
+
Pair the node by running it with no args and following the prompt
|
|
211
|
+
to vault.add.ai/entity/connect/<CODE>.
|
|
212
212
|
`);
|
|
213
213
|
return;
|
|
214
214
|
}
|
package/dist/session-runner.js
CHANGED
|
@@ -2198,6 +2198,21 @@ async function runRequest(req) {
|
|
|
2198
2198
|
catch {
|
|
2199
2199
|
/* drive manifest is best-effort — never block a run */
|
|
2200
2200
|
}
|
|
2201
|
+
// Living Presence: entities that carry the entity-self MCP narrate their work on
|
|
2202
|
+
// table records via its `report_status` tool. Injected HERE — the one place every
|
|
2203
|
+
// spawn path reads (req.system_prompt) — so it reaches EVERY session, board and
|
|
2204
|
+
// channel from a single source, never per-board. Gated on the tool actually being
|
|
2205
|
+
// installed, so it can't confuse entities that don't have it.
|
|
2206
|
+
if (installed.mcps?.some((m) => m.slug === 'entity-self-mcp')) {
|
|
2207
|
+
const livingPresence = [
|
|
2208
|
+
'LIVING PRESENCE — narrate your work so people can see what you are doing in real time.',
|
|
2209
|
+
'Whenever you are working on a specific table record (a card / task / row), call the `report_status` tool at your real milestones: when you claim/start it (status "working", a low progress, a short human note like "Reviewing"), at each checkpoint (bump progress + update the note), if you get stuck and need a human (status "blocked", note why), and when you finish (status "done"). ALWAYS pass the record_id you are working on; if you are working on several records at once, report each separately with its own record_id so they never overwrite each other. This is lightweight — a handful of milestone updates per task; progress is a rough milestone percent (0-100), not a precise measurement.',
|
|
2210
|
+
].join('\n');
|
|
2211
|
+
req.system_prompt =
|
|
2212
|
+
req.system_prompt && req.system_prompt.trim().length > 0
|
|
2213
|
+
? `${req.system_prompt}\n\n${livingPresence}`
|
|
2214
|
+
: livingPresence;
|
|
2215
|
+
}
|
|
2201
2216
|
// Same for the entity's long-term memory: try the +Ai Memory semantic pack
|
|
2202
2217
|
// (T6's entity-memory-pack edge function) first — it replaces the flat
|
|
2203
2218
|
// DocFlows title list with a curated recall block for memory_v2-flagged
|
package/dist/tui/dashboard.d.ts
CHANGED
|
@@ -7,7 +7,15 @@ export interface DashboardState {
|
|
|
7
7
|
/** Recent requests, newest first — the NOW band reads the live ones off
|
|
8
8
|
* the top and the idle line reads the most recent finish. */
|
|
9
9
|
recent: RequestRow[];
|
|
10
|
+
/** Cursor position in the menu. */
|
|
10
11
|
sel: number;
|
|
12
|
+
/** Id of the selected live run, or null when the cursor is in the menu.
|
|
13
|
+
* Pinned by id rather than by index because the band reorders under the
|
|
14
|
+
* cursor: a new run is prepended the moment it starts, and an index would
|
|
15
|
+
* quietly leave you pointing at a different run than the one you chose. */
|
|
16
|
+
nowSelId: string | null;
|
|
17
|
+
/** Live rows the last render could fit — what bounds the cursor. */
|
|
18
|
+
nowRows: number;
|
|
11
19
|
spin: number;
|
|
12
20
|
pid: number | null;
|
|
13
21
|
startedAt: number | null;
|
|
@@ -35,6 +43,19 @@ export declare const MENU: Array<{
|
|
|
35
43
|
/** Live rows the NOW band shows before it starts counting the rest. */
|
|
36
44
|
export declare const MAX_NOW_ROWS = 6;
|
|
37
45
|
export declare function liveRequests(rows: RequestRow[]): RequestRow[];
|
|
46
|
+
/** The live rows the band actually draws — the only ones the cursor can reach.
|
|
47
|
+
* Everything past the cap lives on the Activity screen. A short terminal
|
|
48
|
+
* draws fewer than MAX_NOW_ROWS, and the cursor must not run off into rows
|
|
49
|
+
* that are not on screen, so the last render's row count is what bounds it. */
|
|
50
|
+
export declare function selectableNow(st: DashboardState): RequestRow[];
|
|
51
|
+
/**
|
|
52
|
+
* Where the cursor is in the NOW band, or -1 for "in the menu".
|
|
53
|
+
*
|
|
54
|
+
* Derived from the pinned id on every read, so a run that finishes while
|
|
55
|
+
* selected drops the cursor back to the menu rather than silently moving it
|
|
56
|
+
* onto whichever run took that row.
|
|
57
|
+
*/
|
|
58
|
+
export declare function nowIndex(st: DashboardState): number;
|
|
38
59
|
/**
|
|
39
60
|
* The node's own state, as a label.
|
|
40
61
|
*
|
package/dist/tui/dashboard.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.MAX_NOW_ROWS = exports.MENU = void 0;
|
|
10
10
|
exports.liveRequests = liveRequests;
|
|
11
|
+
exports.selectableNow = selectableNow;
|
|
12
|
+
exports.nowIndex = nowIndex;
|
|
11
13
|
exports.stateLabel = stateLabel;
|
|
12
14
|
exports.autostartLine = autostartLine;
|
|
13
15
|
exports.fmtCount = fmtCount;
|
|
@@ -29,6 +31,26 @@ exports.MAX_NOW_ROWS = 6;
|
|
|
29
31
|
function liveRequests(rows) {
|
|
30
32
|
return rows.filter(r => request_row_1.ACTIVE.has(r.status));
|
|
31
33
|
}
|
|
34
|
+
/** The live rows the band actually draws — the only ones the cursor can reach.
|
|
35
|
+
* Everything past the cap lives on the Activity screen. A short terminal
|
|
36
|
+
* draws fewer than MAX_NOW_ROWS, and the cursor must not run off into rows
|
|
37
|
+
* that are not on screen, so the last render's row count is what bounds it. */
|
|
38
|
+
function selectableNow(st) {
|
|
39
|
+
const cap = Math.max(1, Math.min(exports.MAX_NOW_ROWS, st.nowRows || exports.MAX_NOW_ROWS));
|
|
40
|
+
return liveRequests(st.recent).slice(0, cap);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Where the cursor is in the NOW band, or -1 for "in the menu".
|
|
44
|
+
*
|
|
45
|
+
* Derived from the pinned id on every read, so a run that finishes while
|
|
46
|
+
* selected drops the cursor back to the menu rather than silently moving it
|
|
47
|
+
* onto whichever run took that row.
|
|
48
|
+
*/
|
|
49
|
+
function nowIndex(st) {
|
|
50
|
+
if (!st.nowSelId)
|
|
51
|
+
return -1;
|
|
52
|
+
return selectableNow(st).findIndex(r => r.id === st.nowSelId);
|
|
53
|
+
}
|
|
32
54
|
/**
|
|
33
55
|
* The node's own state, as a label.
|
|
34
56
|
*
|
|
@@ -128,7 +150,10 @@ function lastFinished(rows) {
|
|
|
128
150
|
}
|
|
129
151
|
function nowLines(st, width, rows) {
|
|
130
152
|
const live = liveRequests(st.recent);
|
|
131
|
-
const
|
|
153
|
+
const heading = live.length
|
|
154
|
+
? ` ${(0, render_1.bold)('Now')} ${(0, render_1.dim)('↑ to select · ⏎ to watch')}`
|
|
155
|
+
: ` ${(0, render_1.bold)('Now')}`;
|
|
156
|
+
const out = [heading];
|
|
132
157
|
if (live.length === 0) {
|
|
133
158
|
const last = lastFinished(st.recent);
|
|
134
159
|
out.push(last
|
|
@@ -138,7 +163,9 @@ function nowLines(st, width, rows) {
|
|
|
138
163
|
}
|
|
139
164
|
out.push((0, request_row_1.requestHeader)(width));
|
|
140
165
|
const shown = live.slice(0, Math.max(1, rows));
|
|
141
|
-
|
|
166
|
+
st.nowRows = shown.length;
|
|
167
|
+
const cursor = nowIndex(st);
|
|
168
|
+
shown.forEach((r, i) => out.push((0, request_row_1.requestLine)(r, st.now, i === cursor, width, st.spin)));
|
|
142
169
|
if (live.length > shown.length) {
|
|
143
170
|
out.push((0, render_1.dim)(` +${live.length - shown.length} more running`));
|
|
144
171
|
}
|
|
@@ -153,8 +180,11 @@ function renderDashboard(st, width, height) {
|
|
|
153
180
|
// The shortcut letters line up in a column of their own, so the eye can
|
|
154
181
|
// find them without reading the hints.
|
|
155
182
|
const hintWidth = Math.max(...exports.MENU.map(m => m.hint.length)) + 2;
|
|
183
|
+
// One cursor on screen at a time: while it is up in the NOW band the menu
|
|
184
|
+
// shows no selection, or the eye reads two.
|
|
185
|
+
const inNow = nowIndex(st) >= 0;
|
|
156
186
|
const menu = exports.MENU.map((m, i) => {
|
|
157
|
-
const selected = i === st.sel;
|
|
187
|
+
const selected = !inNow && i === st.sel;
|
|
158
188
|
const label = selected ? (0, render_1.bold)(m.label.padEnd(11)) : m.label.padEnd(11);
|
|
159
189
|
const cursor = selected ? (0, render_1.cyan)('❯') : ' ';
|
|
160
190
|
const badge = m.key === 'logs' && st.logCount ? `${st.logCount} lines` : '';
|
|
@@ -162,7 +192,7 @@ function renderDashboard(st, width, height) {
|
|
|
162
192
|
});
|
|
163
193
|
const foot = (0, app_1.footerHint)([
|
|
164
194
|
{ keys: '↑↓', label: 'move' },
|
|
165
|
-
{ keys: '⏎', label: 'open' },
|
|
195
|
+
{ keys: '⏎', label: inNow ? 'watch this run' : 'open' },
|
|
166
196
|
{ keys: 's', label: st.autostart?.enabled ? 'startup off' : 'startup on' },
|
|
167
197
|
{ keys: 'r', label: 'refresh' },
|
|
168
198
|
{ keys: '?', label: 'keys' },
|
|
@@ -202,6 +232,11 @@ function createDashboardScreen(deps) {
|
|
|
202
232
|
if (recent.length)
|
|
203
233
|
st.recent = recent;
|
|
204
234
|
st.offline = deps.data.offline();
|
|
235
|
+
// A selected run that has finished is no longer in the band. nowIndex
|
|
236
|
+
// already reads that as "cursor is in the menu"; drop the id too so the
|
|
237
|
+
// state doesn't keep pointing at a run nobody can see.
|
|
238
|
+
if (st.nowSelId && !selectableNow(st).some(r => r.id === st.nowSelId))
|
|
239
|
+
st.nowSelId = null;
|
|
205
240
|
// Reads a file (and, on Windows, a cached schtasks query) — cheap enough
|
|
206
241
|
// to ride the same poll, so the header can't disagree with reality after
|
|
207
242
|
// someone changes it from Studio or the CLI.
|
|
@@ -242,6 +277,43 @@ function createDashboardScreen(deps) {
|
|
|
242
277
|
// line — a note that never clears becomes furniture.
|
|
243
278
|
setTimeout(() => { st.autostartNote = null; deps.host.redraw(); }, 8000).unref?.();
|
|
244
279
|
};
|
|
280
|
+
/**
|
|
281
|
+
* One cursor over two stacked lists: the NOW band sits above the menu, so
|
|
282
|
+
* ↑ off the top of the menu lands on the LAST live row (the one nearest the
|
|
283
|
+
* menu) and ↓ off the bottom of the band returns to the first menu item.
|
|
284
|
+
* Reading the screen top to bottom, the cursor moves the way the eye does.
|
|
285
|
+
*/
|
|
286
|
+
const moveUp = () => {
|
|
287
|
+
const live = selectableNow(st);
|
|
288
|
+
const i = nowIndex(st);
|
|
289
|
+
if (i < 0) {
|
|
290
|
+
// In the menu. Step up inside it first; only leave from the top row.
|
|
291
|
+
if (st.sel > 0) {
|
|
292
|
+
st.sel -= 1;
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (live.length)
|
|
296
|
+
st.nowSelId = live[live.length - 1].id;
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (i > 0)
|
|
300
|
+
st.nowSelId = live[i - 1].id;
|
|
301
|
+
};
|
|
302
|
+
const moveDown = () => {
|
|
303
|
+
const live = selectableNow(st);
|
|
304
|
+
const i = nowIndex(st);
|
|
305
|
+
if (i < 0) {
|
|
306
|
+
st.sel = Math.min(exports.MENU.length - 1, st.sel + 1);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
// Past the last live row is the menu, entered at its first item.
|
|
310
|
+
if (i >= live.length - 1) {
|
|
311
|
+
st.nowSelId = null;
|
|
312
|
+
st.sel = 0;
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
st.nowSelId = live[i + 1].id;
|
|
316
|
+
};
|
|
245
317
|
return {
|
|
246
318
|
id: 'dashboard',
|
|
247
319
|
title: '+Ai Node',
|
|
@@ -258,8 +330,8 @@ function createDashboardScreen(deps) {
|
|
|
258
330
|
return true;
|
|
259
331
|
},
|
|
260
332
|
keys: () => [
|
|
261
|
-
{ keys: '↑↓ / jk', label: 'move
|
|
262
|
-
{ keys: '⏎', label: 'open the selected
|
|
333
|
+
{ keys: '↑↓ / jk', label: 'move — up from the menu into the live runs' },
|
|
334
|
+
{ keys: '⏎', label: 'open the destination, or watch the selected run' },
|
|
263
335
|
{ keys: 'a', label: 'activity — full request history' },
|
|
264
336
|
{ keys: 'h', label: 'harnesses — install / log in agent CLIs' },
|
|
265
337
|
{ keys: 'l', label: 'logs — daemon output' },
|
|
@@ -268,12 +340,12 @@ function createDashboardScreen(deps) {
|
|
|
268
340
|
],
|
|
269
341
|
async onKey(key) {
|
|
270
342
|
if (key.name === 'up' || key.name === 'k') {
|
|
271
|
-
|
|
343
|
+
moveUp();
|
|
272
344
|
deps.host.redraw();
|
|
273
345
|
return;
|
|
274
346
|
}
|
|
275
347
|
if (key.name === 'down' || key.name === 'j') {
|
|
276
|
-
|
|
348
|
+
moveDown();
|
|
277
349
|
deps.host.redraw();
|
|
278
350
|
return;
|
|
279
351
|
}
|
|
@@ -298,6 +370,12 @@ function createDashboardScreen(deps) {
|
|
|
298
370
|
return;
|
|
299
371
|
}
|
|
300
372
|
if (key.name === 'return') {
|
|
373
|
+
// A selected run wins over the menu: the cursor is visibly on it.
|
|
374
|
+
const live = selectableNow(st)[nowIndex(st)];
|
|
375
|
+
if (live) {
|
|
376
|
+
deps.openTranscript(live);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
301
379
|
const target = exports.MENU[st.sel]?.key;
|
|
302
380
|
if (target === 'harnesses')
|
|
303
381
|
deps.openHarnesses();
|
package/dist/tui/run.js
CHANGED
|
@@ -192,7 +192,7 @@ async function runDashboard(opts) {
|
|
|
192
192
|
// the daemon log file, so nothing is lost by watching.
|
|
193
193
|
const logs = (0, console_capture_1.captureConsole)({ logFile: paths_1.RUNTIME_LOG_FILE });
|
|
194
194
|
const state = {
|
|
195
|
-
self: null, stats: null, recent: [], sel: 0, spin: 0,
|
|
195
|
+
self: null, stats: null, recent: [], sel: 0, nowSelId: null, nowRows: 0, spin: 0,
|
|
196
196
|
pid: opts.pid, startedAt: opts.startedAt,
|
|
197
197
|
inflight: 0, paired: (0, store_1.isPaired)(), viewerMode: opts.viewerMode,
|
|
198
198
|
offline: false, now: Date.now(), version: opts.version,
|