@phnx-labs/agents-cli 1.20.91 → 1.20.92
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 +155 -0
- package/README.md +1 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/activity.d.ts +72 -6
- package/dist/commands/activity.js +198 -49
- package/dist/commands/beta.js +1 -0
- package/dist/commands/doctor.js +4 -2
- package/dist/commands/exec.d.ts +14 -0
- package/dist/commands/exec.js +144 -14
- package/dist/commands/projects.d.ts +12 -0
- package/dist/commands/projects.js +358 -0
- package/dist/commands/sessions-picker.d.ts +15 -0
- package/dist/commands/sessions-picker.js +37 -12
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +9 -1
- package/dist/commands/sessions.d.ts +10 -5
- package/dist/commands/sessions.js +65 -27
- package/dist/index.js +2 -1
- package/dist/lib/activity.d.ts +69 -12
- package/dist/lib/activity.js +417 -74
- package/dist/lib/beta.d.ts +1 -1
- package/dist/lib/beta.js +1 -1
- package/dist/lib/devices/registry.d.ts +14 -0
- package/dist/lib/devices/registry.js +37 -0
- package/dist/lib/feed-post.js +8 -2
- package/dist/lib/hosts/remote-cmd.js +4 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +14 -4
- package/dist/lib/menubar/install-menubar.js +20 -6
- package/dist/lib/project-key.d.ts +44 -0
- package/dist/lib/project-key.js +79 -0
- package/dist/lib/project-root.js +16 -0
- package/dist/lib/project-status.d.ts +69 -0
- package/dist/lib/project-status.js +101 -0
- package/dist/lib/projects.d.ts +138 -0
- package/dist/lib/projects.js +301 -0
- package/dist/lib/remote-agents-json.d.ts +9 -0
- package/dist/lib/remote-agents-json.js +11 -5
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/session/bash-command.d.ts +53 -0
- package/dist/lib/session/bash-command.js +364 -0
- package/dist/lib/session/digest.d.ts +6 -0
- package/dist/lib/session/digest.js +19 -0
- package/dist/lib/session/relative-time.d.ts +23 -0
- package/dist/lib/session/relative-time.js +60 -8
- package/dist/lib/session/remote-list.js +5 -2
- package/dist/lib/session/render.d.ts +2 -9
- package/dist/lib/session/render.js +25 -56
- package/dist/lib/ssh-exec.d.ts +6 -0
- package/dist/lib/ssh-exec.js +10 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +5 -0
- package/dist/lib/terminal/backends/index.d.ts +10 -2
- package/dist/lib/terminal/backends/index.js +14 -2
- package/dist/lib/terminal/backends/terminal-app.d.ts +13 -0
- package/dist/lib/terminal/backends/terminal-app.js +73 -0
- package/dist/lib/terminal/index.d.ts +2 -1
- package/dist/lib/terminal/index.js +2 -1
- package/dist/lib/terminal/preferred.d.ts +89 -0
- package/dist/lib/terminal/preferred.js +87 -0
- package/dist/lib/terminal/run-surface.d.ts +82 -0
- package/dist/lib/terminal/run-surface.js +146 -0
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/types.d.ts +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Named project definitions — the layer above the `--project <slug>` convention.
|
|
3
|
+
*
|
|
4
|
+
* `agents run --project <slug>` already resolves a bare name to a working
|
|
5
|
+
* directory by pure convention (`<projectRoot>/<slug>`, see `project-root.ts`).
|
|
6
|
+
* This module adds editable definitions on top: one YAML file per project under
|
|
7
|
+
* `~/.agents/projects/<name>.yaml`, sitting beside the existing `routines/`,
|
|
8
|
+
* `monitors/`, and `teams/` dirs in the user repo (so definitions sync across
|
|
9
|
+
* machines for free via `agents push/pull`). A defined project can name itself
|
|
10
|
+
* independently of its folder, bind more than one repo, pin a monorepo subpath,
|
|
11
|
+
* describe context subdirectories an agent should start from, carry a Linear
|
|
12
|
+
* link and external integrations, and set an explicit default path.
|
|
13
|
+
*
|
|
14
|
+
* Portable by construction: `root`/`defaultPath` are stored home-relative
|
|
15
|
+
* (`~/…`) via `toHomeRelative`, so the same definition re-roots on any machine
|
|
16
|
+
* whose home differs — the exact mechanism `project-root.ts` already relies on.
|
|
17
|
+
*
|
|
18
|
+
* Resolution stays additive: an undefined slug still resolves exactly as today
|
|
19
|
+
* (see `resolveProjectRef`), a defined one overrides it.
|
|
20
|
+
*/
|
|
21
|
+
import * as fs from 'fs';
|
|
22
|
+
import * as path from 'path';
|
|
23
|
+
import * as yaml from 'yaml';
|
|
24
|
+
import { getProjectsDir } from './state.js';
|
|
25
|
+
import { safeJoin } from './paths.js';
|
|
26
|
+
import { toHomeRelative, expandLocalHome } from './project-root.js';
|
|
27
|
+
import { resolveProjectKey } from './project-key.js';
|
|
28
|
+
/** A project name safe to use as a filename: no separators, `..`, or leading dot. */
|
|
29
|
+
export function isSafeProjectName(name) {
|
|
30
|
+
return (typeof name === 'string' &&
|
|
31
|
+
name.length > 0 &&
|
|
32
|
+
name.length <= 64 &&
|
|
33
|
+
/^[a-z0-9][a-z0-9._-]*$/i.test(name) &&
|
|
34
|
+
name !== '.' &&
|
|
35
|
+
name !== '..');
|
|
36
|
+
}
|
|
37
|
+
/** Absolute path to a project's YAML definition. Throws on an unsafe name. */
|
|
38
|
+
export function projectDefPath(name) {
|
|
39
|
+
if (!isSafeProjectName(name)) {
|
|
40
|
+
throw new Error(`Invalid project name: "${name}" (letters, digits, ., _, - only)`);
|
|
41
|
+
}
|
|
42
|
+
return safeJoin(getProjectsDir(), `${name}.yaml`);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Validate a raw parsed object into a `ProjectDef`, throwing an actionable error
|
|
46
|
+
* on the first problem. A malformed document or identity (bad/mismatched name)
|
|
47
|
+
* throws; malformed entries inside the optional lists (`repos`/`contexts`/
|
|
48
|
+
* `integrations`) are dropped so one bad row can't sink an otherwise good def.
|
|
49
|
+
*/
|
|
50
|
+
export function validateProjectDef(raw, sourceName) {
|
|
51
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
52
|
+
throw new Error(`Project ${sourceName ?? ''} is not a YAML mapping`.trim());
|
|
53
|
+
}
|
|
54
|
+
const o = raw;
|
|
55
|
+
// The filename is the identity; a `name:` field is optional but, when present,
|
|
56
|
+
// must be a valid slug — a malformed one is a loud error, not a silent fallback.
|
|
57
|
+
const hasNameField = 'name' in o && o.name !== undefined && o.name !== null;
|
|
58
|
+
let name;
|
|
59
|
+
if (hasNameField) {
|
|
60
|
+
if (typeof o.name !== 'string' || !isSafeProjectName(o.name)) {
|
|
61
|
+
throw new Error(`Project ${sourceName ?? ''}: "name" must be a valid slug (got ${JSON.stringify(o.name)})`);
|
|
62
|
+
}
|
|
63
|
+
name = o.name;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
name = sourceName;
|
|
67
|
+
}
|
|
68
|
+
if (!name || !isSafeProjectName(name)) {
|
|
69
|
+
throw new Error('Project definition is missing a valid "name"');
|
|
70
|
+
}
|
|
71
|
+
// The filename IS the stable id — a def whose `name:` disagrees with its
|
|
72
|
+
// filename would resolve under one name and list under another.
|
|
73
|
+
if (hasNameField && sourceName && name !== sourceName) {
|
|
74
|
+
throw new Error(`Project ${sourceName}: "name" (${JSON.stringify(name)}) must match the filename — the filename is the stable id`);
|
|
75
|
+
}
|
|
76
|
+
const def = { name };
|
|
77
|
+
if (typeof o.description === 'string')
|
|
78
|
+
def.description = o.description;
|
|
79
|
+
if (typeof o.root === 'string')
|
|
80
|
+
def.root = o.root;
|
|
81
|
+
if (typeof o.defaultPath === 'string')
|
|
82
|
+
def.defaultPath = o.defaultPath;
|
|
83
|
+
if (typeof o.repo === 'string')
|
|
84
|
+
def.repo = o.repo;
|
|
85
|
+
if (Array.isArray(o.repos)) {
|
|
86
|
+
def.repos = o.repos.flatMap((r) => {
|
|
87
|
+
if (r && typeof r === 'object' && typeof r.slug === 'string') {
|
|
88
|
+
const rr = r;
|
|
89
|
+
const repo = { slug: rr.slug };
|
|
90
|
+
if (typeof rr.subpath === 'string')
|
|
91
|
+
repo.subpath = rr.subpath;
|
|
92
|
+
return [repo];
|
|
93
|
+
}
|
|
94
|
+
return [];
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (Array.isArray(o.contexts)) {
|
|
98
|
+
def.contexts = o.contexts.flatMap((c) => {
|
|
99
|
+
if (c &&
|
|
100
|
+
typeof c === 'object' &&
|
|
101
|
+
typeof c.path === 'string' &&
|
|
102
|
+
typeof c.purpose === 'string') {
|
|
103
|
+
const cc = c;
|
|
104
|
+
return [{ path: cc.path, purpose: cc.purpose }];
|
|
105
|
+
}
|
|
106
|
+
return [];
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (Array.isArray(o.integrations)) {
|
|
110
|
+
def.integrations = o.integrations.flatMap((i) => {
|
|
111
|
+
if (i &&
|
|
112
|
+
typeof i === 'object' &&
|
|
113
|
+
typeof i.kind === 'string' &&
|
|
114
|
+
typeof i.url === 'string') {
|
|
115
|
+
const ii = i;
|
|
116
|
+
const integ = { kind: ii.kind, url: ii.url };
|
|
117
|
+
if (typeof ii.label === 'string')
|
|
118
|
+
integ.label = ii.label;
|
|
119
|
+
return [integ];
|
|
120
|
+
}
|
|
121
|
+
return [];
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (o.linear && typeof o.linear === 'object' && !Array.isArray(o.linear)) {
|
|
125
|
+
const l = o.linear;
|
|
126
|
+
def.linear = {};
|
|
127
|
+
if (typeof l.projectId === 'string')
|
|
128
|
+
def.linear.projectId = l.projectId;
|
|
129
|
+
if (typeof l.url === 'string')
|
|
130
|
+
def.linear.url = l.url;
|
|
131
|
+
}
|
|
132
|
+
if (Array.isArray(o.docs))
|
|
133
|
+
def.docs = o.docs.filter((d) => typeof d === 'string');
|
|
134
|
+
return def;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Load a single project definition by name. Returns undefined when the file is
|
|
138
|
+
* absent (the common "not a defined project, fall back to convention" case) but
|
|
139
|
+
* throws when a file EXISTS and is malformed — a broken definition is loud.
|
|
140
|
+
*/
|
|
141
|
+
export function loadProjectDef(name) {
|
|
142
|
+
if (!isSafeProjectName(name))
|
|
143
|
+
return undefined;
|
|
144
|
+
let raw;
|
|
145
|
+
try {
|
|
146
|
+
raw = fs.readFileSync(projectDefPath(name), 'utf8');
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return undefined; // absent — not a defined project
|
|
150
|
+
}
|
|
151
|
+
return validateProjectDef(yaml.parse(raw), name);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* List every defined project, sorted by name. Skips (does not throw on) a
|
|
155
|
+
* malformed file so one bad definition can't break `projects list`; the loader
|
|
156
|
+
* for a single named project stays strict.
|
|
157
|
+
*/
|
|
158
|
+
export function listProjectDefs() {
|
|
159
|
+
let files;
|
|
160
|
+
try {
|
|
161
|
+
// Definitions are `<name>.yaml` (what projectDefPath/loadProjectDef read). We
|
|
162
|
+
// deliberately do NOT list `.yml` here — accepting it would then ENOENT in the
|
|
163
|
+
// loader and silently drop the project. One extension, one code path.
|
|
164
|
+
files = fs.readdirSync(getProjectsDir()).filter((f) => f.endsWith('.yaml'));
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
const out = [];
|
|
170
|
+
for (const f of files) {
|
|
171
|
+
const name = f.replace(/\.yaml$/, '');
|
|
172
|
+
try {
|
|
173
|
+
const def = loadProjectDef(name);
|
|
174
|
+
if (def)
|
|
175
|
+
out.push(def);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
/* malformed — skip in the listing */
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Persist a project definition, normalizing `root`/`defaultPath` to home-relative
|
|
185
|
+
* so it stays portable across machines. Creates the projects dir on first write.
|
|
186
|
+
*/
|
|
187
|
+
export function writeProjectDef(def) {
|
|
188
|
+
const validated = validateProjectDef(def, def.name);
|
|
189
|
+
const normalized = {
|
|
190
|
+
...validated,
|
|
191
|
+
root: validated.root ? toHomeRelative(expandLocalHome(validated.root)) : undefined,
|
|
192
|
+
defaultPath: validated.defaultPath
|
|
193
|
+
? toHomeRelative(expandLocalHome(validated.defaultPath))
|
|
194
|
+
: undefined,
|
|
195
|
+
};
|
|
196
|
+
// Drop undefined keys so the YAML stays clean.
|
|
197
|
+
const clean = Object.fromEntries(Object.entries(normalized).filter(([, v]) => v !== undefined));
|
|
198
|
+
const target = projectDefPath(def.name);
|
|
199
|
+
fs.mkdirSync(getProjectsDir(), { recursive: true });
|
|
200
|
+
fs.writeFileSync(target, yaml.stringify(clean), 'utf8');
|
|
201
|
+
return target;
|
|
202
|
+
}
|
|
203
|
+
/** Delete a project definition. Returns true if a file was removed. Never touches the repo. */
|
|
204
|
+
export function removeProjectDef(name) {
|
|
205
|
+
try {
|
|
206
|
+
fs.unlinkSync(projectDefPath(name));
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* The cwd an agent lands in for a defined project: `defaultPath` when set, else
|
|
215
|
+
* `root`. Home-relative when `forRemote` (the remote shell expands `~`), else
|
|
216
|
+
* expanded against the local home. Returns undefined when neither is set.
|
|
217
|
+
*/
|
|
218
|
+
export function projectBasePath(def, forRemote) {
|
|
219
|
+
const base = def.defaultPath ?? def.root;
|
|
220
|
+
if (!base)
|
|
221
|
+
return undefined;
|
|
222
|
+
return forRemote ? base : expandLocalHome(base);
|
|
223
|
+
}
|
|
224
|
+
function projectRootsAbs(defs) {
|
|
225
|
+
const out = [];
|
|
226
|
+
for (const def of defs) {
|
|
227
|
+
const raw = def.root ?? def.defaultPath;
|
|
228
|
+
if (!raw)
|
|
229
|
+
continue;
|
|
230
|
+
out.push({ name: def.name, abs: path.resolve(expandLocalHome(raw)) });
|
|
231
|
+
}
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
/** True when `child` is `parent` or nested under it (path-segment aware). */
|
|
235
|
+
function isUnder(child, parent) {
|
|
236
|
+
if (child === parent)
|
|
237
|
+
return true;
|
|
238
|
+
const withSep = parent.endsWith(path.sep) ? parent : parent + path.sep;
|
|
239
|
+
return child.startsWith(withSep);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Which defined project a session belongs to, derived from its working
|
|
243
|
+
* directory. A session whose `cwd` sits inside a project's repo root (or a
|
|
244
|
+
* worktree under it) is a member; the LONGEST matching root wins so a nested
|
|
245
|
+
* project beats its parent. Returns undefined when no definition contains the
|
|
246
|
+
* path.
|
|
247
|
+
*
|
|
248
|
+
* The comparison is against the LOCAL home: roots and the cwd are both expanded
|
|
249
|
+
* with `expandLocalHome` and resolved, so this matches sessions whose cwd shares
|
|
250
|
+
* this machine's home layout. A session recorded on a different-home machine
|
|
251
|
+
* (`/Users/x/…` vs `/home/x/…`) will not match until the fleet-wide,
|
|
252
|
+
* home-relative variant lands (see the deferred item in docs/11-projects.md).
|
|
253
|
+
*/
|
|
254
|
+
export function projectNameForCwd(cwd, defs) {
|
|
255
|
+
if (!cwd)
|
|
256
|
+
return undefined;
|
|
257
|
+
const abs = path.resolve(expandLocalHome(cwd));
|
|
258
|
+
let best;
|
|
259
|
+
let bestLen = -1;
|
|
260
|
+
for (const { name, abs: root } of projectRootsAbs(defs)) {
|
|
261
|
+
if (isUnder(abs, root) && root.length > bestLen) {
|
|
262
|
+
best = name;
|
|
263
|
+
bestLen = root.length;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return best;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* The canonical project label for a cwd, for every surface that buckets work by
|
|
270
|
+
* project (the activity timeline, feed posts, the sessions overview): the
|
|
271
|
+
* DEFINED project whose root contains the cwd (longest root wins, so a
|
|
272
|
+
* multi-repo project reads as one bucket), else the repository-level key from
|
|
273
|
+
* {@link resolveProjectKey}. `defs` comes from {@link listProjectDefs}, which is
|
|
274
|
+
* fail-open — with no definitions this degrades to exactly today's behavior.
|
|
275
|
+
*/
|
|
276
|
+
export function resolveProjectNameForCwd(cwd, defs) {
|
|
277
|
+
if (!cwd)
|
|
278
|
+
return undefined;
|
|
279
|
+
return projectNameForCwd(cwd, defs) ?? resolveProjectKey(cwd);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Resolve a defined project's ref to a working directory, mirroring
|
|
283
|
+
* `buildProjectPath`'s `forRemote` contract (a home-relative `~/…` for the
|
|
284
|
+
* remote shell to expand, an absolute local path otherwise). A `@worktree`
|
|
285
|
+
* lands under the repo ROOT's `.agents/worktrees/`, not the `defaultPath`
|
|
286
|
+
* subdir — worktrees are per-repo, not per-focus. Returns undefined when the
|
|
287
|
+
* definition carries no `root`/`defaultPath` (caller falls back to convention).
|
|
288
|
+
*/
|
|
289
|
+
export function resolveDefinedProjectPath(def, worktree, forRemote) {
|
|
290
|
+
if (worktree) {
|
|
291
|
+
const rootRaw = def.root ?? def.defaultPath;
|
|
292
|
+
if (!rootRaw)
|
|
293
|
+
return undefined;
|
|
294
|
+
const wt = `${rootRaw}/.agents/worktrees/${worktree}`;
|
|
295
|
+
return forRemote ? wt : path.resolve(expandLocalHome(wt));
|
|
296
|
+
}
|
|
297
|
+
const base = projectBasePath(def, forRemote);
|
|
298
|
+
if (!base)
|
|
299
|
+
return undefined;
|
|
300
|
+
return forRemote ? base : path.resolve(base);
|
|
301
|
+
}
|
|
@@ -3,10 +3,19 @@ export interface RemoteAgentsJsonOptions<T> {
|
|
|
3
3
|
noFanoutEnv: string;
|
|
4
4
|
hosts?: string[];
|
|
5
5
|
parse: (stdout: string, machine: string) => T[];
|
|
6
|
+
/**
|
|
7
|
+
* Suppress the per-device "unreachable — skipped" stderr line. The skipped
|
|
8
|
+
* names still come back in {@link RemoteAgentsJsonResult.skipped}, so a caller
|
|
9
|
+
* that fans out by DEFAULT can report them once, compactly, instead of
|
|
10
|
+
* printing a line per offline box above its output. Never a silent drop.
|
|
11
|
+
*/
|
|
12
|
+
quiet?: boolean;
|
|
6
13
|
}
|
|
7
14
|
export interface RemoteAgentsJsonResult<T> {
|
|
8
15
|
items: T[];
|
|
9
16
|
deviceCount: number;
|
|
17
|
+
/** Devices that were dialed but answered with an error / no CLI / a timeout. */
|
|
18
|
+
skipped: string[];
|
|
10
19
|
}
|
|
11
20
|
/** Build the command one peer runs, with a guard that prevents recursive fan-out. */
|
|
12
21
|
export declare function remoteAgentsJsonCommand(args: string[], noFanoutEnv: string, os?: string): string;
|
|
@@ -11,7 +11,7 @@ import chalk from 'chalk';
|
|
|
11
11
|
import { SSH_OPTS, controlOpts, assertValidSshTarget, shellQuote } from './ssh-exec.js';
|
|
12
12
|
import { sshTargetFor } from './devices/connect.js';
|
|
13
13
|
import { resolveExplicitTargets } from './devices/resolve-target.js';
|
|
14
|
-
import { loadDevices, isControlDevice } from './devices/registry.js';
|
|
14
|
+
import { loadDevices, isControlDevice, isDialableDevice } from './devices/registry.js';
|
|
15
15
|
import { remoteShellFor, buildWindowsAgentsCommand } from './hosts/remote-cmd.js';
|
|
16
16
|
import { machineId, normalizeHost } from './machine-id.js';
|
|
17
17
|
const REMOTE_TIMEOUT_MS = 12_000;
|
|
@@ -59,10 +59,12 @@ export async function gatherRemoteAgentsJson(options) {
|
|
|
59
59
|
devices = await loadDevices();
|
|
60
60
|
}
|
|
61
61
|
catch {
|
|
62
|
-
return { items: [], deviceCount: 0 };
|
|
62
|
+
return { items: [], deviceCount: 0, skipped: [] };
|
|
63
63
|
}
|
|
64
64
|
for (const device of Object.values(devices)) {
|
|
65
|
-
|
|
65
|
+
// Live SSH-probe verdict first, cached tailscale snapshot only as a
|
|
66
|
+
// fallback — see isDialableDevice (mirrors session/remote-list.ts).
|
|
67
|
+
if (!isDialableDevice(device))
|
|
66
68
|
continue;
|
|
67
69
|
if (normalizeHost(device.name) === self)
|
|
68
70
|
continue;
|
|
@@ -87,14 +89,18 @@ export async function gatherRemoteAgentsJson(options) {
|
|
|
87
89
|
}
|
|
88
90
|
}
|
|
89
91
|
}
|
|
92
|
+
const skipped = [];
|
|
90
93
|
const results = await Promise.all(targets.map(async (target) => {
|
|
91
94
|
const command = remoteAgentsJsonCommand(options.args, options.noFanoutEnv, target.os);
|
|
92
95
|
const result = await sshCapture(target.target, command);
|
|
93
96
|
if (result.code !== 0) {
|
|
94
|
-
|
|
97
|
+
skipped.push(target.name);
|
|
98
|
+
if (!options.quiet) {
|
|
99
|
+
process.stderr.write(chalk.gray(` ${target.name}: unreachable or no agents CLI — skipped\n`));
|
|
100
|
+
}
|
|
95
101
|
return [];
|
|
96
102
|
}
|
|
97
103
|
return options.parse(result.stdout, target.machine);
|
|
98
104
|
}));
|
|
99
|
-
return { items: results.flat(), deviceCount: targets.length };
|
|
105
|
+
return { items: results.flat(), deviceCount: targets.length, skipped };
|
|
100
106
|
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses the raw command strings agents pass to Bash tool calls into structured
|
|
3
|
+
* metadata: the executable, category, subcommand, and a display summary. Used by
|
|
4
|
+
* session rendering and the activity-log hook so `agents sessions` and
|
|
5
|
+
* `agents activity` can summarize what actually happened instead of printing a
|
|
6
|
+
* wall of shell.
|
|
7
|
+
*/
|
|
8
|
+
export type BashCategory = 'vcs' | 'build-test' | 'install' | 'remote' | 'http' | 'media' | 'upscaling' | 'metadata' | 'probe' | 'search' | 'shell' | 'wait' | 'other';
|
|
9
|
+
export interface BashToolInfo {
|
|
10
|
+
category: BashCategory;
|
|
11
|
+
signal: 'high' | 'mid' | 'low';
|
|
12
|
+
action: string;
|
|
13
|
+
aliases?: string[];
|
|
14
|
+
}
|
|
15
|
+
export interface BashCommandInfo {
|
|
16
|
+
tool: string;
|
|
17
|
+
category: BashCategory;
|
|
18
|
+
subcommand: string;
|
|
19
|
+
action: string;
|
|
20
|
+
summary: string;
|
|
21
|
+
signal: 'high' | 'mid' | 'low';
|
|
22
|
+
}
|
|
23
|
+
export declare function unwrapCommand(cmd: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Split a possibly-compound Bash command into simple commands, then tokenize each
|
|
26
|
+
* with shlex. Returns an empty array for empty input.
|
|
27
|
+
*/
|
|
28
|
+
export declare function tokenizeBash(cmd: string): string[][];
|
|
29
|
+
/**
|
|
30
|
+
* Classify the first simple command in a Bash string. Returns coarse metadata
|
|
31
|
+
* (tool name, category, subcommand, human action) used for summaries and
|
|
32
|
+
* activity logging. Unknown executables fall back to `other`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function classifyBashCommand(command: string): BashCommandInfo;
|
|
35
|
+
/**
|
|
36
|
+
* Stable bucket key for grouping similar Bash commands in summaries. Commands run
|
|
37
|
+
* through a remote wrapper (ssh/scp/rsync) get an `ssh→` prefix on the inner key,
|
|
38
|
+
* so `ssh host "git push"` buckets as `ssh→git push`, distinct from a local push.
|
|
39
|
+
*/
|
|
40
|
+
export declare function bucketKey(command: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* Detect high-signal Bash-driven milestones (video renders, upscales, metadata
|
|
43
|
+
* edits, git commits/pushes/worktrees, PR opens). Returns null for routine
|
|
44
|
+
* commands.
|
|
45
|
+
*/
|
|
46
|
+
export declare function detectBashMilestone(command: string): {
|
|
47
|
+
event: string;
|
|
48
|
+
detail: string;
|
|
49
|
+
} | null;
|
|
50
|
+
/**
|
|
51
|
+
* Human-readable category label for renderers.
|
|
52
|
+
*/
|
|
53
|
+
export declare function categoryLabel(category: BashCategory): string;
|