@phnx-labs/agents-cli 1.20.72 → 1.20.74
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/CHANGELOG.md +395 -0
- package/README.md +40 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/activity.d.ts +12 -0
- package/dist/commands/activity.js +68 -0
- package/dist/commands/apply.d.ts +13 -0
- package/dist/commands/apply.js +28 -3
- package/dist/commands/browser.js +1 -1
- package/dist/commands/cloud.js +21 -11
- package/dist/commands/doctor.js +21 -5
- package/dist/commands/events.d.ts +11 -10
- package/dist/commands/events.js +30 -21
- package/dist/commands/exec.d.ts +34 -0
- package/dist/commands/exec.js +337 -39
- package/dist/commands/export.d.ts +11 -0
- package/dist/commands/export.js +215 -0
- package/dist/commands/feed.d.ts +13 -0
- package/dist/commands/feed.js +130 -29
- package/dist/commands/hq.d.ts +2 -0
- package/dist/commands/hq.js +58 -0
- package/dist/commands/import.js +51 -3
- package/dist/commands/lease.d.ts +19 -0
- package/dist/commands/lease.js +138 -11
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +123 -0
- package/dist/commands/logs.js +1 -1
- package/dist/commands/mine.d.ts +27 -0
- package/dist/commands/mine.js +207 -0
- package/dist/commands/monitors.js +79 -49
- package/dist/commands/plugins.js +1 -0
- package/dist/commands/profiles.js +158 -0
- package/dist/commands/repo.js +2 -60
- package/dist/commands/resources.d.ts +5 -0
- package/dist/commands/resources.js +95 -0
- package/dist/commands/routines.d.ts +2 -0
- package/dist/commands/routines.js +139 -37
- package/dist/commands/secrets.js +234 -47
- package/dist/commands/send.d.ts +15 -0
- package/dist/commands/send.js +62 -0
- package/dist/commands/sessions-tail.d.ts +1 -0
- package/dist/commands/sessions-tail.js +20 -9
- package/dist/commands/sessions.js +18 -3
- package/dist/commands/setup-mine.d.ts +18 -0
- package/dist/commands/setup-mine.js +106 -0
- package/dist/commands/setup-share.js +2 -2
- package/dist/commands/setup.js +9 -2
- package/dist/commands/share.d.ts +8 -1
- package/dist/commands/share.js +122 -37
- package/dist/commands/ssh.d.ts +9 -0
- package/dist/commands/ssh.js +196 -26
- package/dist/commands/sync.js +1 -1
- package/dist/commands/teams.js +11 -2
- package/dist/commands/versions.js +36 -5
- package/dist/commands/view.js +75 -27
- package/dist/index.js +80 -10
- package/dist/lib/activity.d.ts +124 -0
- package/dist/lib/activity.js +542 -0
- package/dist/lib/agents.d.ts +35 -0
- package/dist/lib/agents.js +79 -6
- package/dist/lib/artifact-actions.d.ts +1 -1
- package/dist/lib/artifact-actions.js +1 -1
- package/dist/lib/ask-classifier.d.ts +2 -1
- package/dist/lib/ask-classifier.js +3 -3
- package/dist/lib/auth-health.d.ts +32 -4
- package/dist/lib/auth-health.js +40 -1
- package/dist/lib/auto-pull.js +8 -1
- package/dist/lib/brand.d.ts +40 -0
- package/dist/lib/brand.js +122 -0
- package/dist/lib/browser/drivers/ssh.js +4 -7
- package/dist/lib/browser/service.js +6 -8
- package/dist/lib/channels/providers/index.d.ts +2 -0
- package/dist/lib/channels/providers/index.js +20 -0
- package/dist/lib/channels/providers/mailbox.d.ts +2 -0
- package/dist/lib/channels/providers/mailbox.js +26 -0
- package/dist/lib/channels/providers/openclaw-telegram.d.ts +2 -0
- package/dist/lib/channels/providers/openclaw-telegram.js +34 -0
- package/dist/lib/channels/providers/rush.d.ts +6 -0
- package/dist/lib/channels/providers/rush.js +55 -0
- package/dist/lib/channels/registry.d.ts +42 -0
- package/dist/lib/channels/registry.js +20 -0
- package/dist/lib/channels/resolve.d.ts +11 -0
- package/dist/lib/channels/resolve.js +12 -0
- package/dist/lib/cli-resources.d.ts +18 -0
- package/dist/lib/cli-resources.js +53 -4
- package/dist/lib/cloud/codex.js +6 -3
- package/dist/lib/cloud/factory.d.ts +1 -0
- package/dist/lib/cloud/factory.js +10 -4
- package/dist/lib/cloud/rush.d.ts +3 -1
- package/dist/lib/cloud/rush.js +6 -1
- package/dist/lib/cloud/types.d.ts +2 -0
- package/dist/lib/codex-home.d.ts +35 -0
- package/dist/lib/codex-home.js +136 -0
- package/dist/lib/config-transfer.d.ts +16 -0
- package/dist/lib/config-transfer.js +56 -0
- package/dist/lib/crabbox/cli.d.ts +44 -0
- package/dist/lib/crabbox/cli.js +120 -20
- package/dist/lib/crabbox/lease.d.ts +52 -1
- package/dist/lib/crabbox/lease.js +117 -16
- package/dist/lib/crabbox/progress.d.ts +31 -0
- package/dist/lib/crabbox/progress.js +76 -0
- package/dist/lib/crabbox/runtimes.d.ts +4 -0
- package/dist/lib/crabbox/runtimes.js +33 -8
- package/dist/lib/crabbox/setup-copy.d.ts +87 -0
- package/dist/lib/crabbox/setup-copy.js +127 -0
- package/dist/lib/daemon.d.ts +5 -5
- package/dist/lib/daemon.js +9 -9
- package/dist/lib/drive-sync.js +6 -3
- package/dist/lib/event-stream.d.ts +36 -0
- package/dist/lib/event-stream.js +68 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +11 -0
- package/dist/lib/exec.js +23 -2
- package/dist/lib/export.d.ts +72 -0
- package/dist/lib/export.js +269 -0
- package/dist/lib/feed-policy.d.ts +1 -1
- package/dist/lib/feed-policy.js +14 -9
- package/dist/lib/feed-ranking.d.ts +32 -0
- package/dist/lib/feed-ranking.js +224 -0
- package/dist/lib/feed.d.ts +35 -2
- package/dist/lib/feed.js +66 -3
- package/dist/lib/fleet/auth-sync.d.ts +53 -0
- package/dist/lib/fleet/auth-sync.js +92 -0
- package/dist/lib/fleet/remote-login.d.ts +187 -0
- package/dist/lib/fleet/remote-login.js +556 -0
- package/dist/lib/format.d.ts +30 -3
- package/dist/lib/format.js +34 -5
- package/dist/lib/fs-atomic.d.ts +7 -2
- package/dist/lib/fs-atomic.js +8 -12
- package/dist/lib/git.d.ts +16 -0
- package/dist/lib/git.js +79 -2
- package/dist/lib/heal.js +11 -3
- package/dist/lib/hosts/progress.d.ts +28 -10
- package/dist/lib/hosts/progress.js +79 -22
- package/dist/lib/hosts/remote-cmd.js +4 -0
- package/dist/lib/hq/floor.d.ts +87 -0
- package/dist/lib/hq/floor.js +226 -0
- package/dist/lib/import.d.ts +23 -0
- package/dist/lib/import.js +74 -2
- package/dist/lib/isolation-boundary-report.d.ts +10 -0
- package/dist/lib/isolation-boundary-report.js +35 -0
- package/dist/lib/menubar/install-menubar.js +14 -20
- package/dist/lib/notify.d.ts +1 -0
- package/dist/lib/notify.js +3 -3
- package/dist/lib/open-url.d.ts +2 -0
- package/dist/lib/open-url.js +19 -0
- package/dist/lib/openclaw-keychain.d.ts +56 -0
- package/dist/lib/openclaw-keychain.js +236 -0
- package/dist/lib/overdue.js +10 -0
- package/dist/lib/permissions.d.ts +44 -1
- package/dist/lib/permissions.js +284 -7
- package/dist/lib/project-launch.d.ts +6 -12
- package/dist/lib/project-launch.js +13 -228
- package/dist/lib/project-resources.d.ts +7 -0
- package/dist/lib/project-resources.js +306 -0
- package/dist/lib/pty-client.d.ts +27 -0
- package/dist/lib/pty-client.js +136 -10
- package/dist/lib/refresh.js +14 -10
- package/dist/lib/resource-profiles.d.ts +26 -0
- package/dist/lib/resource-profiles.js +157 -0
- package/dist/lib/resources/permissions.js +7 -1
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/resources.d.ts +1 -1
- package/dist/lib/resources.js +32 -3
- package/dist/lib/routines.d.ts +11 -0
- package/dist/lib/routines.js +56 -15
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.js +66 -5
- package/dist/lib/secrets/bundles.d.ts +16 -3
- package/dist/lib/secrets/bundles.js +206 -37
- package/dist/lib/secrets/icloud-import.d.ts +2 -2
- package/dist/lib/secrets/icloud-import.js +9 -6
- package/dist/lib/secrets/mcp.js +1 -1
- package/dist/lib/secrets/vault-age-helper.d.ts +1 -0
- package/dist/lib/secrets/vault-age-helper.js +34 -0
- package/dist/lib/secrets/vault.d.ts +49 -0
- package/dist/lib/secrets/vault.js +397 -0
- package/dist/lib/self-heal/checks/path.js +3 -1
- package/dist/lib/self-heal/checks/shadowing.js +10 -2
- package/dist/lib/self-heal/checks/shims.js +19 -11
- package/dist/lib/session/cloud.d.ts +2 -2
- package/dist/lib/session/cloud.js +2 -2
- package/dist/lib/session/db.d.ts +4 -0
- package/dist/lib/session/db.js +44 -7
- package/dist/lib/session/discover.d.ts +2 -0
- package/dist/lib/session/discover.js +114 -3
- package/dist/lib/session/stream-render.d.ts +3 -0
- package/dist/lib/session/stream-render.js +130 -0
- package/dist/lib/session/types.d.ts +6 -0
- package/dist/lib/share/analytics.d.ts +13 -0
- package/dist/lib/share/analytics.js +45 -0
- package/dist/lib/share/config.d.ts +19 -5
- package/dist/lib/share/config.js +44 -8
- package/dist/lib/share/provision.d.ts +58 -6
- package/dist/lib/share/provision.js +97 -22
- package/dist/lib/share/publish.d.ts +36 -13
- package/dist/lib/share/publish.js +55 -8
- package/dist/lib/share/worker-template.js +83 -17
- package/dist/lib/shims.d.ts +35 -0
- package/dist/lib/shims.js +159 -13
- package/dist/lib/ssh-exec.d.ts +14 -0
- package/dist/lib/ssh-exec.js +57 -0
- package/dist/lib/staleness/detectors/hooks.js +25 -1
- package/dist/lib/staleness/detectors/permissions.js +66 -0
- package/dist/lib/staleness/detectors/workflows.js +20 -0
- package/dist/lib/staleness/writers/hooks.js +58 -4
- package/dist/lib/staleness/writers/permissions.js +2 -2
- package/dist/lib/staleness/writers/skills.js +1 -1
- package/dist/lib/staleness/writers/sources.d.ts +10 -1
- package/dist/lib/staleness/writers/sources.js +68 -1
- package/dist/lib/star-nudge.d.ts +45 -0
- package/dist/lib/star-nudge.js +91 -0
- package/dist/lib/startup/command-registry.d.ts +7 -1
- package/dist/lib/startup/command-registry.js +19 -3
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +17 -4
- package/dist/lib/subagents-registry.d.ts +2 -0
- package/dist/lib/subagents-registry.js +3 -0
- package/dist/lib/teams/parsers.js +214 -6
- package/dist/lib/teams/supervisor.d.ts +7 -0
- package/dist/lib/teams/supervisor.js +2 -1
- package/dist/lib/template.d.ts +1 -1
- package/dist/lib/template.js +1 -1
- package/dist/lib/triggers/webhook.js +36 -3
- package/dist/lib/types.d.ts +84 -0
- package/dist/lib/uninstall.js +1 -45
- package/dist/lib/version.d.ts +40 -0
- package/dist/lib/version.js +94 -16
- package/dist/lib/versions.d.ts +24 -0
- package/dist/lib/versions.js +269 -78
- package/dist/lib/workflows.d.ts +2 -0
- package/dist/lib/workflows.js +88 -0
- package/package.json +2 -1
- package/dist/commands/daemon.d.ts +0 -10
- package/dist/commands/daemon.js +0 -121
package/dist/lib/fs-atomic.js
CHANGED
|
@@ -41,9 +41,9 @@ export function ensureLockTarget(filePath, initialContent = '', dirMode) {
|
|
|
41
41
|
* Writes content to filePath via a temp file + rename so readers never see a
|
|
42
42
|
* partial write. On POSIX, rename(2) is atomic.
|
|
43
43
|
*/
|
|
44
|
-
export function atomicWriteFileSync(filePath, content) {
|
|
44
|
+
export function atomicWriteFileSync(filePath, content, options = 'utf-8') {
|
|
45
45
|
const tmpPath = `${filePath}.tmp-${process.pid}-${randomBytes(8).toString('hex')}`;
|
|
46
|
-
fs.writeFileSync(tmpPath, content,
|
|
46
|
+
fs.writeFileSync(tmpPath, content, options);
|
|
47
47
|
try {
|
|
48
48
|
fs.renameSync(tmpPath, filePath);
|
|
49
49
|
}
|
|
@@ -55,19 +55,15 @@ export function atomicWriteFileSync(filePath, content) {
|
|
|
55
55
|
throw err;
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
-
|
|
59
|
-
* Acquires an exclusive proper-lockfile lock on filePath, runs fn, then
|
|
60
|
-
* releases the lock. Retries with capped linear back-off until either the lock
|
|
61
|
-
* is acquired or LOCK_ACQUIRE_TIMEOUT_MS elapses. Breaks stale locks older than
|
|
62
|
-
* LOCK_STALE_MS, so a crashed holder never blocks past the stale window.
|
|
63
|
-
*/
|
|
64
|
-
export function withFileLock(filePath, fn) {
|
|
58
|
+
export function withFileLock(filePath, fn, opts = {}) {
|
|
65
59
|
let release = null;
|
|
66
60
|
let lastError;
|
|
67
|
-
const
|
|
61
|
+
const staleMs = opts.staleMs ?? LOCK_STALE_MS;
|
|
62
|
+
const acquireTimeoutMs = opts.acquireTimeoutMs ?? LOCK_ACQUIRE_TIMEOUT_MS;
|
|
63
|
+
const deadline = Date.now() + acquireTimeoutMs;
|
|
68
64
|
for (let attempt = 0;; attempt++) {
|
|
69
65
|
try {
|
|
70
|
-
release = lockfile.lockSync(filePath, { stale:
|
|
66
|
+
release = lockfile.lockSync(filePath, { stale: staleMs });
|
|
71
67
|
break;
|
|
72
68
|
}
|
|
73
69
|
catch (err) {
|
|
@@ -80,7 +76,7 @@ export function withFileLock(filePath, fn) {
|
|
|
80
76
|
}
|
|
81
77
|
if (!release) {
|
|
82
78
|
const message = lastError instanceof Error ? lastError.message : String(lastError);
|
|
83
|
-
throw new Error(`Could not acquire lock for ${filePath} after ${
|
|
79
|
+
throw new Error(`Could not acquire lock for ${filePath} after ${acquireTimeoutMs}ms: ${message}`);
|
|
84
80
|
}
|
|
85
81
|
try {
|
|
86
82
|
return fn();
|
package/dist/lib/git.d.ts
CHANGED
|
@@ -91,6 +91,22 @@ export declare function getRepoCommit(repoPath: string): Promise<string>;
|
|
|
91
91
|
* Returns null if gh is not installed or user is not authenticated.
|
|
92
92
|
*/
|
|
93
93
|
export declare function getGitHubUsername(): Promise<string | null>;
|
|
94
|
+
/**
|
|
95
|
+
* Resolve the GitHub username synchronously for `agents share`. Order:
|
|
96
|
+
* 1. `AGENTS_SHARE_GITHUB_USER` env override
|
|
97
|
+
* 2. `git config --global github.user`
|
|
98
|
+
* Falls back to null so callers can decide whether to require auth or proceed
|
|
99
|
+
* without a namespace.
|
|
100
|
+
*/
|
|
101
|
+
export declare function resolveGitHubUsernameSync(): string | null;
|
|
102
|
+
/**
|
|
103
|
+
* Resolve the GitHub username asynchronously. Order:
|
|
104
|
+
* 1. `AGENTS_SHARE_GITHUB_USER` env override
|
|
105
|
+
* 2. `gh api user --jq ".login"`
|
|
106
|
+
* 3. `git config --global github.user`
|
|
107
|
+
* Returns null if none succeed.
|
|
108
|
+
*/
|
|
109
|
+
export declare function resolveGitHubUsername(): Promise<string | null>;
|
|
94
110
|
/**
|
|
95
111
|
* Get the remote URL for origin in a git repo.
|
|
96
112
|
*/
|
package/dist/lib/git.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* source parsing for GitHub shorthand, SSH, HTTPS, and local paths.
|
|
7
7
|
*/
|
|
8
8
|
import simpleGit from 'simple-git';
|
|
9
|
+
import { execFileSync } from 'node:child_process';
|
|
9
10
|
import * as fs from 'fs';
|
|
10
11
|
import * as os from 'os';
|
|
11
12
|
import * as path from 'path';
|
|
@@ -350,6 +351,47 @@ export async function getGitHubUsername() {
|
|
|
350
351
|
return null;
|
|
351
352
|
}
|
|
352
353
|
}
|
|
354
|
+
const GITHUB_USER_ENV = 'AGENTS_SHARE_GITHUB_USER';
|
|
355
|
+
/** Best-effort sync read of `github.user` from git config. */
|
|
356
|
+
function readGitConfigUser() {
|
|
357
|
+
try {
|
|
358
|
+
return execFileSync('git', ['config', '--global', 'github.user'], { stdio: ['ignore', 'pipe', 'ignore'] })
|
|
359
|
+
.toString()
|
|
360
|
+
.trim() || null;
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Resolve the GitHub username synchronously for `agents share`. Order:
|
|
368
|
+
* 1. `AGENTS_SHARE_GITHUB_USER` env override
|
|
369
|
+
* 2. `git config --global github.user`
|
|
370
|
+
* Falls back to null so callers can decide whether to require auth or proceed
|
|
371
|
+
* without a namespace.
|
|
372
|
+
*/
|
|
373
|
+
export function resolveGitHubUsernameSync() {
|
|
374
|
+
const env = process.env[GITHUB_USER_ENV]?.trim();
|
|
375
|
+
if (env)
|
|
376
|
+
return env;
|
|
377
|
+
return readGitConfigUser();
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Resolve the GitHub username asynchronously. Order:
|
|
381
|
+
* 1. `AGENTS_SHARE_GITHUB_USER` env override
|
|
382
|
+
* 2. `gh api user --jq ".login"`
|
|
383
|
+
* 3. `git config --global github.user`
|
|
384
|
+
* Returns null if none succeed.
|
|
385
|
+
*/
|
|
386
|
+
export async function resolveGitHubUsername() {
|
|
387
|
+
const env = process.env[GITHUB_USER_ENV]?.trim();
|
|
388
|
+
if (env)
|
|
389
|
+
return env;
|
|
390
|
+
const gh = await getGitHubUsername();
|
|
391
|
+
if (gh)
|
|
392
|
+
return gh;
|
|
393
|
+
return readGitConfigUser();
|
|
394
|
+
}
|
|
353
395
|
/**
|
|
354
396
|
* Get the remote URL for origin in a git repo.
|
|
355
397
|
*/
|
|
@@ -779,12 +821,47 @@ export async function pullRepo(dir) {
|
|
|
779
821
|
return {
|
|
780
822
|
success: false,
|
|
781
823
|
commit: '',
|
|
782
|
-
error: `
|
|
824
|
+
error: `Blocked by local changes. Commit or discard them before pulling.\n\n cd ${displayHomePath(dir)} && git status`,
|
|
783
825
|
};
|
|
784
826
|
}
|
|
785
827
|
const branch = status.current || 'main';
|
|
786
828
|
await git.fetch('origin');
|
|
787
|
-
|
|
829
|
+
// Resolve the upstream ref to fast-forward against. Prefer the local
|
|
830
|
+
// branch's tracking config; otherwise ask origin for its default branch.
|
|
831
|
+
let tracking = status.tracking;
|
|
832
|
+
if (!tracking) {
|
|
833
|
+
try {
|
|
834
|
+
await git.raw(['remote', 'set-head', 'origin', '--auto']);
|
|
835
|
+
const sym = await git.raw(['symbolic-ref', '--short', 'refs/remotes/origin/HEAD']);
|
|
836
|
+
tracking = sym.trim();
|
|
837
|
+
}
|
|
838
|
+
catch {
|
|
839
|
+
tracking = `origin/${branch}`;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
const localRef = await git.revparse(['HEAD']);
|
|
843
|
+
const remoteRef = await git.revparse([tracking]).catch(() => null);
|
|
844
|
+
if (!remoteRef) {
|
|
845
|
+
return { success: false, commit: '', error: `Could not resolve upstream ref ${tracking}` };
|
|
846
|
+
}
|
|
847
|
+
if (localRef === remoteRef) {
|
|
848
|
+
const log = await git.log({ maxCount: 1 });
|
|
849
|
+
return {
|
|
850
|
+
success: true,
|
|
851
|
+
commit: log.latest?.hash.slice(0, 8) || 'unknown',
|
|
852
|
+
branch,
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
try {
|
|
856
|
+
await git.merge(['--ff-only', tracking]);
|
|
857
|
+
}
|
|
858
|
+
catch {
|
|
859
|
+
return {
|
|
860
|
+
success: false,
|
|
861
|
+
commit: '',
|
|
862
|
+
error: `Blocked by local commits. Push or reset them before pulling.\n\n cd ${displayHomePath(dir)} && git log --oneline HEAD...${tracking}`,
|
|
863
|
+
};
|
|
864
|
+
}
|
|
788
865
|
installGithooksSymlinks(dir);
|
|
789
866
|
const log = await git.log({ maxCount: 1 });
|
|
790
867
|
return {
|
package/dist/lib/heal.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* `agents prune cleanup`, so a heal pass can never lose work.
|
|
24
24
|
*/
|
|
25
25
|
import { ALL_AGENT_IDS } from './agents.js';
|
|
26
|
-
import { syncResourcesToVersion, listInstalledVersions, getVersionHomePath, getActuallySyncedResources, compareVersions, } from './versions.js';
|
|
26
|
+
import { syncResourcesToVersion, listInstalledVersions, isVersionIsolated, getVersionHomePath, getActuallySyncedResources, compareVersions, } from './versions.js';
|
|
27
27
|
import { diffVersionResources, } from './doctor-diff.js';
|
|
28
28
|
import { discoverPlugins, updatePlugin, readPluginSourceInfo, getUpstreamManifestVersion, } from './plugins.js';
|
|
29
29
|
import { repairPluginManifestFile } from './plugin-marketplace.js';
|
|
@@ -266,9 +266,17 @@ export async function heal(opts) {
|
|
|
266
266
|
...repairedManifests.map((r) => r.plugin),
|
|
267
267
|
...refreshed.map((r) => r.plugin),
|
|
268
268
|
]);
|
|
269
|
+
// Isolated copies are excluded from every SWEEP. `agents add --isolated`
|
|
270
|
+
// promises "no settings carry-over, no resource sync", and this engine runs
|
|
271
|
+
// unattended from the daemon (~30s after start, then every ~6h) — a sweep that
|
|
272
|
+
// walked into an isolated home would quietly refill it with shared commands,
|
|
273
|
+
// skills, hooks, MCP config and permissions. Explicitly NAMED versions
|
|
274
|
+
// (`agents doctor <agent>@<version> --fix`) are honoured as-is: naming the
|
|
275
|
+
// version is the operator's consent.
|
|
276
|
+
const sweep = (a) => listInstalledVersions(a).filter((v) => !isVersionIsolated(a, v));
|
|
269
277
|
const targets = opts.agent
|
|
270
|
-
? [{ agent: opts.agent, versions: opts.versions ??
|
|
271
|
-
: ALL_AGENT_IDS.map((a) => ({ agent: a, versions:
|
|
278
|
+
? [{ agent: opts.agent, versions: opts.versions ?? sweep(opts.agent) }]
|
|
279
|
+
: ALL_AGENT_IDS.map((a) => ({ agent: a, versions: sweep(a) }));
|
|
272
280
|
const versions = [];
|
|
273
281
|
for (const t of targets) {
|
|
274
282
|
for (const v of t.versions) {
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Follow a dispatched host run by
|
|
2
|
+
* Follow a dispatched host run by streaming its remote log.
|
|
3
3
|
*
|
|
4
4
|
* The run writes combined output to a log file on the host and its exit code to
|
|
5
|
-
* a sibling `.exit` file.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* a sibling `.exit` file. The primary path opens one long-lived
|
|
6
|
+
* `ssh ... tail -f` stream (durable, offset-tracked — a dropped connection
|
|
7
|
+
* reconnects from the saved offset) and finishes when the remote-side watcher
|
|
8
|
+
* emits the exit sentinel. Rich transcript-parser rendering is a fast-follow.
|
|
8
9
|
*
|
|
9
|
-
* Efficiency:
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* socket (multiplex on) that the launch opened, and eases the poll interval off
|
|
13
|
-
* toward `maxPollMs` while the job is idle, so a quiet long-running follow no
|
|
14
|
-
* longer spawns thousands of ssh processes per hour on the laptop.
|
|
10
|
+
* Efficiency: a healthy follow holds exactly one ssh process/socket for the
|
|
11
|
+
* whole run instead of spawning once per poll cycle. If the connection drops,
|
|
12
|
+
* the next stream resumes at the byte offset already flushed locally.
|
|
15
13
|
*/
|
|
16
14
|
/**
|
|
17
15
|
* Cap for the LOCAL mirror of a distributed teammate's remote log. `followHostTask`
|
|
@@ -102,6 +100,26 @@ export declare function fetchProgress(target: string, opts: {
|
|
|
102
100
|
logChunk: Buffer;
|
|
103
101
|
exit: string;
|
|
104
102
|
} | null;
|
|
103
|
+
/**
|
|
104
|
+
* Remote shell for the persistent follow stream.
|
|
105
|
+
*
|
|
106
|
+
* Protocol: stream `tail -c +<offset+1> -f <log>` as raw stdout until `.exit`
|
|
107
|
+
* becomes non-empty, give tail one more polling interval to flush final bytes,
|
|
108
|
+
* stop it, then print the same per-task sentinel and `cat` the exit file on
|
|
109
|
+
* stderr. Keeping the terminal frame off stdout lets log bytes pass through
|
|
110
|
+
* live without buffering and without confusing agent output for our marker.
|
|
111
|
+
* `remoteLog`/`remoteExit` are dispatch-generated `$HOME` paths with safe hex
|
|
112
|
+
* basenames (or absolute paths in localhost integration tests), so they stay
|
|
113
|
+
* unquoted to preserve `$HOME` expansion just like `fetchProgress`.
|
|
114
|
+
*/
|
|
115
|
+
export declare function buildStreamingFollowCommand(opts: {
|
|
116
|
+
remoteLog: string;
|
|
117
|
+
remoteExit: string;
|
|
118
|
+
taskId: string;
|
|
119
|
+
offset: number;
|
|
120
|
+
}): string;
|
|
121
|
+
/** Extract the remote watcher frame from stderr, if the stream ended normally. */
|
|
122
|
+
export declare function parseStreamingExitFrame(stderr: Buffer, taskId: string): Buffer | null;
|
|
105
123
|
/**
|
|
106
124
|
* File identity (`dev:ino`) of a path on the remote host, or null if it can't be
|
|
107
125
|
* stat'd. GNU (`-c`) then BSD (`-f`) format, so it works on Linux and macOS hosts.
|
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Follow a dispatched host run by
|
|
2
|
+
* Follow a dispatched host run by streaming its remote log.
|
|
3
3
|
*
|
|
4
4
|
* The run writes combined output to a log file on the host and its exit code to
|
|
5
|
-
* a sibling `.exit` file.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* a sibling `.exit` file. The primary path opens one long-lived
|
|
6
|
+
* `ssh ... tail -f` stream (durable, offset-tracked — a dropped connection
|
|
7
|
+
* reconnects from the saved offset) and finishes when the remote-side watcher
|
|
8
|
+
* emits the exit sentinel. Rich transcript-parser rendering is a fast-follow.
|
|
8
9
|
*
|
|
9
|
-
* Efficiency:
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* socket (multiplex on) that the launch opened, and eases the poll interval off
|
|
13
|
-
* toward `maxPollMs` while the job is idle, so a quiet long-running follow no
|
|
14
|
-
* longer spawns thousands of ssh processes per hour on the laptop.
|
|
10
|
+
* Efficiency: a healthy follow holds exactly one ssh process/socket for the
|
|
11
|
+
* whole run instead of spawning once per poll cycle. If the connection drops,
|
|
12
|
+
* the next stream resumes at the byte offset already flushed locally.
|
|
15
13
|
*/
|
|
16
14
|
import * as fs from 'fs';
|
|
17
|
-
import { sshExec, sshExecRaw } from '../ssh-exec.js';
|
|
15
|
+
import { sshExec, sshExecRaw, sshExecRawStream } from '../ssh-exec.js';
|
|
18
16
|
import { localLogPath } from './tasks.js';
|
|
19
17
|
function sleep(ms) {
|
|
20
18
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -54,6 +52,7 @@ export function pullRemoteLogDelta(target, opts) {
|
|
|
54
52
|
return null; // ssh itself failed / timed out
|
|
55
53
|
return { bytes: res.stdout, newOffset: opts.offset + res.stdout.length };
|
|
56
54
|
}
|
|
55
|
+
const STREAM_EXIT_POLL_SECONDS = 1;
|
|
57
56
|
/**
|
|
58
57
|
* Build the per-task sentinel that separates the log tail from the exit-file
|
|
59
58
|
* contents in one combined fetch. The task id (8 hex chars) makes collision with
|
|
@@ -116,6 +115,47 @@ export function fetchProgress(target, opts) {
|
|
|
116
115
|
return null;
|
|
117
116
|
return { logChunk: parts.logChunk, exit: parts.exit.toString('utf8') };
|
|
118
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Remote shell for the persistent follow stream.
|
|
120
|
+
*
|
|
121
|
+
* Protocol: stream `tail -c +<offset+1> -f <log>` as raw stdout until `.exit`
|
|
122
|
+
* becomes non-empty, give tail one more polling interval to flush final bytes,
|
|
123
|
+
* stop it, then print the same per-task sentinel and `cat` the exit file on
|
|
124
|
+
* stderr. Keeping the terminal frame off stdout lets log bytes pass through
|
|
125
|
+
* live without buffering and without confusing agent output for our marker.
|
|
126
|
+
* `remoteLog`/`remoteExit` are dispatch-generated `$HOME` paths with safe hex
|
|
127
|
+
* basenames (or absolute paths in localhost integration tests), so they stay
|
|
128
|
+
* unquoted to preserve `$HOME` expansion just like `fetchProgress`.
|
|
129
|
+
*/
|
|
130
|
+
export function buildStreamingFollowCommand(opts) {
|
|
131
|
+
const printfArg = exitMarker(opts.taskId).replace(/\n/g, '\\n');
|
|
132
|
+
return [
|
|
133
|
+
'set +e',
|
|
134
|
+
'tail_pid=',
|
|
135
|
+
'cleanup() { if [ -n "$tail_pid" ]; then kill "$tail_pid" 2>/dev/null || true; wait "$tail_pid" 2>/dev/null || true; fi; }',
|
|
136
|
+
'trap cleanup EXIT HUP INT TERM',
|
|
137
|
+
`while [ ! -e ${opts.remoteLog} ] && [ ! -s ${opts.remoteExit} ]; do sleep ${STREAM_EXIT_POLL_SECONDS}; done`,
|
|
138
|
+
`tail -c +${opts.offset + 1} -f ${opts.remoteLog} 2>/dev/null &`,
|
|
139
|
+
'tail_pid=$!',
|
|
140
|
+
`while [ ! -s ${opts.remoteExit} ]; do`,
|
|
141
|
+
' if ! kill -0 "$tail_pid" 2>/dev/null; then wait "$tail_pid" 2>/dev/null; exit 86; fi',
|
|
142
|
+
` sleep ${STREAM_EXIT_POLL_SECONDS}`,
|
|
143
|
+
'done',
|
|
144
|
+
`sleep ${STREAM_EXIT_POLL_SECONDS}`,
|
|
145
|
+
'cleanup',
|
|
146
|
+
'tail_pid=',
|
|
147
|
+
`printf '${printfArg}' >&2`,
|
|
148
|
+
`cat ${opts.remoteExit} >&2 2>/dev/null`,
|
|
149
|
+
].join('\n');
|
|
150
|
+
}
|
|
151
|
+
/** Extract the remote watcher frame from stderr, if the stream ended normally. */
|
|
152
|
+
export function parseStreamingExitFrame(stderr, taskId) {
|
|
153
|
+
const marker = Buffer.from(exitMarker(taskId), 'utf8');
|
|
154
|
+
const idx = stderr.lastIndexOf(marker);
|
|
155
|
+
if (idx === -1)
|
|
156
|
+
return null;
|
|
157
|
+
return stderr.subarray(idx + marker.length);
|
|
158
|
+
}
|
|
119
159
|
/**
|
|
120
160
|
* File identity (`dev:ino`) of a path on the remote host, or null if it can't be
|
|
121
161
|
* stat'd. GNU (`-c`) then BSD (`-f`) format, so it works on Linux and macOS hosts.
|
|
@@ -169,23 +209,40 @@ export async function followHostTask(target, opts) {
|
|
|
169
209
|
return true;
|
|
170
210
|
};
|
|
171
211
|
for (;;) {
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
212
|
+
const remaining = deadline - Date.now();
|
|
213
|
+
if (remaining <= 0) {
|
|
214
|
+
process.stderr.write('\n[hosts] follow timed out; the run continues on the host. Reattach with: agents hosts logs ' + opts.taskId + ' -f\n');
|
|
215
|
+
return -1;
|
|
216
|
+
}
|
|
217
|
+
const abort = new AbortController();
|
|
218
|
+
const timer = setTimeout(() => abort.abort(), remaining);
|
|
219
|
+
let gotOutput = false;
|
|
220
|
+
let exitFrame = null;
|
|
221
|
+
try {
|
|
222
|
+
const stream = await sshExecRawStream(target, buildStreamingFollowCommand({ remoteLog: opts.remoteLog, remoteExit: opts.remoteExit, taskId: opts.taskId, offset }), {
|
|
223
|
+
timeoutMs: remaining,
|
|
224
|
+
signal: abort.signal,
|
|
225
|
+
multiplex: true,
|
|
226
|
+
onStdout: (chunk) => { gotOutput = flush(chunk) || gotOutput; },
|
|
227
|
+
});
|
|
228
|
+
exitFrame = parseStreamingExitFrame(stream.stderr, opts.taskId);
|
|
229
|
+
}
|
|
230
|
+
finally {
|
|
231
|
+
clearTimeout(timer);
|
|
232
|
+
}
|
|
233
|
+
if (exitFrame) {
|
|
234
|
+
const code = parseInt(exitFrame.toString('utf8').trim(), 10);
|
|
181
235
|
return Number.isFinite(code) ? code : 0;
|
|
182
236
|
}
|
|
237
|
+
// SSH dropped before the remote watcher emitted the exit sentinel. Stdout
|
|
238
|
+
// chunks were flushed as they arrived, so reconnect from the advanced offset.
|
|
239
|
+
// If no bytes arrived, back off like the old idle poll.
|
|
183
240
|
if (Date.now() > deadline) {
|
|
184
241
|
process.stderr.write('\n[hosts] follow timed out; the run continues on the host. Reattach with: agents hosts logs ' + opts.taskId + ' -f\n');
|
|
185
242
|
return -1;
|
|
186
243
|
}
|
|
187
|
-
//
|
|
188
|
-
//
|
|
244
|
+
// Reconnect quickly while output flows; ease toward maxMs when the stream
|
|
245
|
+
// drops without bytes so a flapping idle host does not spin ssh processes.
|
|
189
246
|
waitMs = gotOutput ? fastMs : Math.min(maxMs, Math.round(waitMs * 1.5));
|
|
190
247
|
await sleep(waitMs);
|
|
191
248
|
}
|
|
@@ -108,7 +108,11 @@ export const RUN_OPTION_FORWARDING = {
|
|
|
108
108
|
any: 'local-only',
|
|
109
109
|
follow: 'local-only',
|
|
110
110
|
lease: 'local-only',
|
|
111
|
+
box: 'local-only',
|
|
111
112
|
keepBox: 'local-only',
|
|
113
|
+
reuse: 'local-only', // reuse-picker choice for --lease; the lease path is always local
|
|
114
|
+
bare: 'local-only', // skips the local setup-copy push; lease-only concern
|
|
115
|
+
tailscale: 'local-only', // --tailscale/--no-tailscale gate the lease net mode; never forwarded
|
|
112
116
|
copyCreds: 'local-only', // copies creds TO the host before dispatch — local concern only
|
|
113
117
|
authCheck: 'local-only', // --no-auth-check gates the local interactive login preflight; --host runs skip that preflight entirely
|
|
114
118
|
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { ActiveSession } from '../session/active.js';
|
|
2
|
+
import type { OpenBlock } from '../feed.js';
|
|
3
|
+
import type { AgentStatusDetail, TaskInfo } from '../teams/api.js';
|
|
4
|
+
export type HqRoomKind = 'team' | 'machine' | 'lobby';
|
|
5
|
+
export type HqAgentMood = 'working' | 'waiting' | 'blocked' | 'celebrating' | 'idle';
|
|
6
|
+
export type HqAmbientKind = 'needs_input' | 'error' | 'pr' | 'team_active' | 'idle_scene';
|
|
7
|
+
export interface HqAction {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
command: string[];
|
|
11
|
+
target: {
|
|
12
|
+
kind: 'agent' | 'room' | 'floor';
|
|
13
|
+
id: string;
|
|
14
|
+
};
|
|
15
|
+
destructive?: boolean;
|
|
16
|
+
input?: Array<{
|
|
17
|
+
name: string;
|
|
18
|
+
label: string;
|
|
19
|
+
placeholder?: string;
|
|
20
|
+
required?: boolean;
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
23
|
+
export interface HqRoom {
|
|
24
|
+
id: string;
|
|
25
|
+
kind: HqRoomKind;
|
|
26
|
+
name: string;
|
|
27
|
+
machine?: string;
|
|
28
|
+
team?: string;
|
|
29
|
+
counts: {
|
|
30
|
+
agents: number;
|
|
31
|
+
needsInput: number;
|
|
32
|
+
running: number;
|
|
33
|
+
failed: number;
|
|
34
|
+
};
|
|
35
|
+
actions: HqAction[];
|
|
36
|
+
}
|
|
37
|
+
export interface HqAgent {
|
|
38
|
+
id: string;
|
|
39
|
+
label: string;
|
|
40
|
+
agent: string;
|
|
41
|
+
roomId: string;
|
|
42
|
+
mood: HqAgentMood;
|
|
43
|
+
status: string;
|
|
44
|
+
activity?: string;
|
|
45
|
+
preview?: string;
|
|
46
|
+
machine?: string;
|
|
47
|
+
team?: string;
|
|
48
|
+
teammate?: string;
|
|
49
|
+
sessionId?: string;
|
|
50
|
+
mailboxId?: string;
|
|
51
|
+
pid?: number;
|
|
52
|
+
prUrl?: string;
|
|
53
|
+
ticketId?: string;
|
|
54
|
+
actions: HqAction[];
|
|
55
|
+
}
|
|
56
|
+
export interface HqAmbientEvent {
|
|
57
|
+
id: string;
|
|
58
|
+
kind: HqAmbientKind;
|
|
59
|
+
roomId: string;
|
|
60
|
+
agentId?: string;
|
|
61
|
+
label: string;
|
|
62
|
+
intensity: 'low' | 'medium' | 'high';
|
|
63
|
+
}
|
|
64
|
+
export interface HqFloorSnapshot {
|
|
65
|
+
version: 1;
|
|
66
|
+
generatedAt: string;
|
|
67
|
+
counters: {
|
|
68
|
+
rooms: number;
|
|
69
|
+
agents: number;
|
|
70
|
+
teams: number;
|
|
71
|
+
needsInput: number;
|
|
72
|
+
failed: number;
|
|
73
|
+
prs: number;
|
|
74
|
+
};
|
|
75
|
+
rooms: HqRoom[];
|
|
76
|
+
agents: HqAgent[];
|
|
77
|
+
ambientEvents: HqAmbientEvent[];
|
|
78
|
+
actions: HqAction[];
|
|
79
|
+
}
|
|
80
|
+
export interface BuildHqFloorInput {
|
|
81
|
+
generatedAt?: Date;
|
|
82
|
+
sessions: ActiveSession[];
|
|
83
|
+
teams: TaskInfo[];
|
|
84
|
+
teammatesByTeam: Map<string, AgentStatusDetail[]>;
|
|
85
|
+
blocks: OpenBlock[];
|
|
86
|
+
}
|
|
87
|
+
export declare function buildHqFloor(input: BuildHqFloorInput): HqFloorSnapshot;
|