@phnx-labs/agents-cli 1.22.14 → 1.22.16
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 +53 -0
- package/README.md +7 -5
- package/dist/bin/agents +0 -0
- package/dist/commands/cloud.js +4 -6
- package/dist/commands/exec.js +96 -11
- package/dist/commands/humans.d.ts +10 -0
- package/dist/commands/humans.js +91 -0
- package/dist/commands/packages.js +90 -22
- package/dist/commands/projects.d.ts +2 -5
- package/dist/commands/projects.js +92 -65
- package/dist/commands/resume.d.ts +17 -0
- package/dist/commands/resume.js +80 -0
- package/dist/commands/routines.js +53 -71
- package/dist/commands/sessions.d.ts +13 -4
- package/dist/commands/sessions.js +37 -19
- package/dist/commands/setup-watchdog.d.ts +3 -0
- package/dist/commands/setup-watchdog.js +54 -0
- package/dist/commands/setup.js +8 -1
- package/dist/commands/ssh.d.ts +11 -0
- package/dist/commands/ssh.js +18 -4
- package/dist/commands/watchdog.d.ts +3 -4
- package/dist/commands/watchdog.js +40 -28
- package/dist/index.js +3 -2
- package/dist/lib/auto-dispatch.d.ts +8 -10
- package/dist/lib/auto-dispatch.js +20 -42
- package/dist/lib/channels/send.d.ts +4 -1
- package/dist/lib/channels/send.js +15 -7
- package/dist/lib/devices/doctor-findings.d.ts +14 -0
- package/dist/lib/devices/doctor-findings.js +92 -51
- package/dist/lib/exec.d.ts +6 -2
- package/dist/lib/exec.js +22 -4
- package/dist/lib/hooks.d.ts +4 -2
- package/dist/lib/hooks.js +173 -23
- package/dist/lib/humans.d.ts +25 -0
- package/dist/lib/humans.js +65 -0
- package/dist/lib/memory.js +2 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/migrate.d.ts +1 -1
- package/dist/lib/migrate.js +116 -7
- package/dist/lib/notify.js +6 -2
- package/dist/lib/permissions.js +12 -10
- package/dist/lib/project-import.d.ts +7 -46
- package/dist/lib/project-import.js +9 -108
- package/dist/lib/projects.d.ts +20 -3
- package/dist/lib/projects.js +36 -18
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +11 -0
- package/dist/lib/routine-activation.d.ts +14 -0
- package/dist/lib/routine-activation.js +78 -0
- package/dist/lib/routines-project.d.ts +1 -1
- package/dist/lib/routines-project.js +3 -9
- package/dist/lib/routines.d.ts +3 -1
- package/dist/lib/routines.js +32 -19
- 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/secrets/bundles.d.ts +11 -0
- package/dist/lib/secrets/bundles.js +122 -5
- package/dist/lib/session/actor-sidecar.d.ts +5 -2
- package/dist/lib/session/actor-sidecar.js +4 -2
- package/dist/lib/session/db.d.ts +2 -1
- package/dist/lib/session/db.js +19 -3
- package/dist/lib/session/types.d.ts +4 -0
- package/dist/lib/staleness/writers/sources.d.ts +6 -1
- package/dist/lib/staleness/writers/sources.js +93 -7
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +5 -0
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +23 -10
- package/dist/lib/types.d.ts +64 -1
- package/dist/lib/versions.js +48 -14
- package/dist/lib/watchdog/rotate.d.ts +1 -1
- package/dist/lib/watchdog/rotate.js +1 -1
- package/package.json +1 -1
- package/dist/lib/watchdog/routine.d.ts +0 -44
- package/dist/lib/watchdog/routine.js +0 -69
|
@@ -1,32 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure builders behind `agents projects import`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* registry** (`~/.agents/factory/projects.json`, an auto-detection that guesses
|
|
7
|
-
* from checkouts on disk). Both funnel through `writeProjectDef`.
|
|
8
|
-
*
|
|
9
|
-
* The Factory registry stamps each row with a `confidence` — and importing every
|
|
10
|
-
* row regardless is what buried the real projects under a dozen guesses
|
|
11
|
-
* (`agents-cleaned-stale2`, a repo cloned from someone else's org, …). So the
|
|
12
|
-
* import is gated on that field, `high` by default.
|
|
4
|
+
* Currently supports one source: **Linear** (projects someone deliberately
|
|
5
|
+
* created on the board). Results funnel through `writeProjectDef`.
|
|
13
6
|
*
|
|
14
7
|
* Everything here is a pure function of its arguments — no fs, no shell, no
|
|
15
|
-
* process env. The command layer
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* against `$HOME`.
|
|
8
|
+
* process env. The command layer shells out to `linear` and hands the rows in;
|
|
9
|
+
* that's what makes these testable against plain fixtures with no mocking.
|
|
10
|
+
* The one exception is `toHomeRelative`, a string rewrite against `$HOME`.
|
|
19
11
|
*/
|
|
20
12
|
import { type ProjectDef } from './projects.js';
|
|
21
13
|
import { type LinearProjectLite } from './linear-projects.js';
|
|
22
|
-
/** Factory's per-row detection confidence, weakest first. */
|
|
23
|
-
export type ImportConfidence = 'low' | 'medium' | 'high';
|
|
24
|
-
/**
|
|
25
|
-
* The floor an import runs at. `any` is what `--all` means — it takes rows that
|
|
26
|
-
* state no confidence at all, which rank below even `low`; without it "import
|
|
27
|
-
* every row regardless of confidence" would quietly drop the unranked ones.
|
|
28
|
-
*/
|
|
29
|
-
export type ImportFloor = ImportConfidence | 'any';
|
|
30
14
|
/** A row the import declined to write, with the reason to print. */
|
|
31
15
|
export interface ImportSkip {
|
|
32
16
|
name: string;
|
|
@@ -39,42 +23,19 @@ export interface ImportPlan {
|
|
|
39
23
|
}
|
|
40
24
|
/** The validated shape of the `import` flags. */
|
|
41
25
|
export interface ImportOptions {
|
|
42
|
-
source: '
|
|
43
|
-
/** Factory only: the lowest confidence that still imports. */
|
|
44
|
-
minConfidence: ImportFloor;
|
|
26
|
+
source: 'linear';
|
|
45
27
|
force: boolean;
|
|
46
28
|
}
|
|
47
29
|
/** The raw commander flags, before validation. */
|
|
48
30
|
export interface RawImportFlags {
|
|
49
|
-
fromFactory?: boolean;
|
|
50
31
|
fromLinear?: boolean;
|
|
51
|
-
minConfidence?: string;
|
|
52
|
-
all?: boolean;
|
|
53
32
|
force?: boolean;
|
|
54
33
|
}
|
|
55
34
|
/**
|
|
56
35
|
* Validate the flag combination, throwing a user-facing message on the first
|
|
57
|
-
* problem.
|
|
58
|
-
* error, never a silent fall back to the default floor.
|
|
36
|
+
* problem.
|
|
59
37
|
*/
|
|
60
38
|
export declare function validateImportOpts(flags: RawImportFlags): ImportOptions;
|
|
61
|
-
/**
|
|
62
|
-
* Plan the Factory import: same field mapping as before (`path`→`root`,
|
|
63
|
-
* `repoSlug`→`repo`, `linearProjectId`→`linear.projectId`), now gated on the
|
|
64
|
-
* row's `confidence`. A row with no confidence field is a guess with no stated
|
|
65
|
-
* strength, so it ranks below every floor and only `--all` takes it.
|
|
66
|
-
*
|
|
67
|
-
* `resolveRemote` reads the checkout's actual `origin` and OVERRIDES the row's
|
|
68
|
-
* `repoSlug`. Factory derives that slug from the checkout path's last two
|
|
69
|
-
* segments (`apps/factory/src/core/projectIndex.ts:19-26`), which is only right
|
|
70
|
-
* when the directory tree happens to mirror the GitHub org. It commonly does
|
|
71
|
-
* not: a checkout at `~/src/github.com/<you>/agents-cli` whose remote is
|
|
72
|
-
* `phnx-labs/agents-cli` imported as `<you>/agents-cli` — a real, different
|
|
73
|
-
* repo — so the card's merged-PR and release lines silently reported a
|
|
74
|
-
* stranger's repository instead of failing. The remote is the only authority on
|
|
75
|
-
* which repo a checkout is; the path is a guess about it.
|
|
76
|
-
*/
|
|
77
|
-
export declare function buildFactoryImportCandidates(rows: unknown[], existing: Map<string, ProjectDef>, opts: Pick<ImportOptions, 'minConfidence' | 'force'>, resolveRemote?: (root: string) => string | undefined): ImportPlan;
|
|
78
39
|
/**
|
|
79
40
|
* Turn a Linear project name into a definition slug: lowercase, every run of
|
|
80
41
|
* unusable characters collapsed to one `-`, trimmed of leading/trailing
|
|
@@ -1,123 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure builders behind `agents projects import`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* registry** (`~/.agents/factory/projects.json`, an auto-detection that guesses
|
|
7
|
-
* from checkouts on disk). Both funnel through `writeProjectDef`.
|
|
8
|
-
*
|
|
9
|
-
* The Factory registry stamps each row with a `confidence` — and importing every
|
|
10
|
-
* row regardless is what buried the real projects under a dozen guesses
|
|
11
|
-
* (`agents-cleaned-stale2`, a repo cloned from someone else's org, …). So the
|
|
12
|
-
* import is gated on that field, `high` by default.
|
|
4
|
+
* Currently supports one source: **Linear** (projects someone deliberately
|
|
5
|
+
* created on the board). Results funnel through `writeProjectDef`.
|
|
13
6
|
*
|
|
14
7
|
* Everything here is a pure function of its arguments — no fs, no shell, no
|
|
15
|
-
* process env. The command layer
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* against `$HOME`.
|
|
8
|
+
* process env. The command layer shells out to `linear` and hands the rows in;
|
|
9
|
+
* that's what makes these testable against plain fixtures with no mocking.
|
|
10
|
+
* The one exception is `toHomeRelative`, a string rewrite against `$HOME`.
|
|
19
11
|
*/
|
|
20
|
-
import { toHomeRelative } from './project-root.js';
|
|
21
12
|
import { isSafeProjectName } from './projects.js';
|
|
22
13
|
import { matchLocalCheckoutExact } from './linear-projects.js';
|
|
23
|
-
const CONFIDENCE_RANK = { any: 0, low: 1, medium: 2, high: 3 };
|
|
24
|
-
/** Rank a raw `confidence` value; anything absent or unrecognized ranks 0 (below every floor). */
|
|
25
|
-
function confidenceRank(raw) {
|
|
26
|
-
return typeof raw === 'string' ? (CONFIDENCE_RANK[raw] ?? 0) : 0;
|
|
27
|
-
}
|
|
28
14
|
/**
|
|
29
15
|
* Validate the flag combination, throwing a user-facing message on the first
|
|
30
|
-
* problem.
|
|
31
|
-
* error, never a silent fall back to the default floor.
|
|
16
|
+
* problem.
|
|
32
17
|
*/
|
|
33
18
|
export function validateImportOpts(flags) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (sources.length > 1)
|
|
38
|
-
throw new Error('--from-linear and --from-factory are mutually exclusive — pick one.');
|
|
39
|
-
const source = sources[0];
|
|
40
|
-
if (source === 'linear' && (flags.all || flags.minConfidence !== undefined)) {
|
|
41
|
-
throw new Error('--all and --min-confidence apply to --from-factory only (Linear rows carry no confidence).');
|
|
42
|
-
}
|
|
43
|
-
if (flags.all && flags.minConfidence !== undefined) {
|
|
44
|
-
throw new Error('--all and --min-confidence are mutually exclusive (--all means --min-confidence low).');
|
|
45
|
-
}
|
|
46
|
-
let minConfidence = 'high';
|
|
47
|
-
if (flags.all)
|
|
48
|
-
minConfidence = 'any';
|
|
49
|
-
else if (flags.minConfidence !== undefined) {
|
|
50
|
-
const v = flags.minConfidence.trim().toLowerCase();
|
|
51
|
-
if (v !== 'low' && v !== 'medium' && v !== 'high') {
|
|
52
|
-
throw new Error(`Invalid --min-confidence "${flags.minConfidence}" (expected low, medium, or high).`);
|
|
53
|
-
}
|
|
54
|
-
minConfidence = v;
|
|
55
|
-
}
|
|
56
|
-
return { source, minConfidence, force: flags.force === true };
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Plan the Factory import: same field mapping as before (`path`→`root`,
|
|
60
|
-
* `repoSlug`→`repo`, `linearProjectId`→`linear.projectId`), now gated on the
|
|
61
|
-
* row's `confidence`. A row with no confidence field is a guess with no stated
|
|
62
|
-
* strength, so it ranks below every floor and only `--all` takes it.
|
|
63
|
-
*
|
|
64
|
-
* `resolveRemote` reads the checkout's actual `origin` and OVERRIDES the row's
|
|
65
|
-
* `repoSlug`. Factory derives that slug from the checkout path's last two
|
|
66
|
-
* segments (`apps/factory/src/core/projectIndex.ts:19-26`), which is only right
|
|
67
|
-
* when the directory tree happens to mirror the GitHub org. It commonly does
|
|
68
|
-
* not: a checkout at `~/src/github.com/<you>/agents-cli` whose remote is
|
|
69
|
-
* `phnx-labs/agents-cli` imported as `<you>/agents-cli` — a real, different
|
|
70
|
-
* repo — so the card's merged-PR and release lines silently reported a
|
|
71
|
-
* stranger's repository instead of failing. The remote is the only authority on
|
|
72
|
-
* which repo a checkout is; the path is a guess about it.
|
|
73
|
-
*/
|
|
74
|
-
export function buildFactoryImportCandidates(rows, existing, opts, resolveRemote = () => undefined) {
|
|
75
|
-
const floor = CONFIDENCE_RANK[opts.minConfidence];
|
|
76
|
-
const defs = [];
|
|
77
|
-
const skipped = [];
|
|
78
|
-
// The registry keys rows by `owner/repo` but names them by basename, so two
|
|
79
|
-
// repos with the same basename in different orgs (grinich/inflow, me/inflow)
|
|
80
|
-
// arrive as two rows called `inflow`. Without this the second silently
|
|
81
|
-
// overwrote the first on disk and the run still reported both as imported.
|
|
82
|
-
const seen = new Set();
|
|
83
|
-
for (const raw of rows) {
|
|
84
|
-
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
85
|
-
continue;
|
|
86
|
-
const o = raw;
|
|
87
|
-
const name = typeof o.name === 'string' ? o.name : undefined;
|
|
88
|
-
if (!name || !isSafeProjectName(name)) {
|
|
89
|
-
skipped.push({ name: name || '(unnamed)', reason: 'not a usable project name' });
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
const rank = confidenceRank(o.confidence);
|
|
93
|
-
if (rank < floor) {
|
|
94
|
-
const stated = typeof o.confidence === 'string' && o.confidence ? `confidence "${o.confidence}"` : 'no confidence field';
|
|
95
|
-
skipped.push({ name, reason: `${stated} is below the "${opts.minConfidence}" floor` });
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
if (seen.has(name)) {
|
|
99
|
-
skipped.push({ name, reason: 'another row in this registry already claimed the name' });
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
if (existing.has(name) && !opts.force) {
|
|
103
|
-
skipped.push({ name, reason: 'already defined — pass --force to overwrite' });
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
seen.add(name);
|
|
107
|
-
const def = { name };
|
|
108
|
-
if (typeof o.path === 'string')
|
|
109
|
-
def.root = toHomeRelative(o.path);
|
|
110
|
-
// The checkout's own remote wins over the registry's path-derived guess;
|
|
111
|
-
// the guess is the fallback for a path with no git remote to ask.
|
|
112
|
-
const fromRemote = typeof o.path === 'string' ? resolveRemote(o.path) : undefined;
|
|
113
|
-
const repo = fromRemote ?? (typeof o.repoSlug === 'string' ? o.repoSlug : undefined);
|
|
114
|
-
if (repo)
|
|
115
|
-
def.repo = repo;
|
|
116
|
-
if (typeof o.linearProjectId === 'string')
|
|
117
|
-
def.linear = { projectId: o.linearProjectId };
|
|
118
|
-
defs.push(def);
|
|
119
|
-
}
|
|
120
|
-
return { defs, skipped };
|
|
19
|
+
if (!flags.fromLinear)
|
|
20
|
+
throw new Error('Pick an import source: --from-linear.');
|
|
21
|
+
return { source: 'linear', force: flags.force === true };
|
|
121
22
|
}
|
|
122
23
|
/**
|
|
123
24
|
* Turn a Linear project name into a definition slug: lowercase, every run of
|
package/dist/lib/projects.d.ts
CHANGED
|
@@ -94,9 +94,25 @@ export interface ProjectDef {
|
|
|
94
94
|
linear?: {
|
|
95
95
|
projectId?: string;
|
|
96
96
|
url?: string;
|
|
97
|
+
name?: string;
|
|
97
98
|
};
|
|
98
99
|
/** Free-form doc links surfaced in `projects show`. */
|
|
99
100
|
docs?: string[];
|
|
101
|
+
/**
|
|
102
|
+
* Auto-dispatch settings. When `enabled` is true and `maxAgents > 0` and the
|
|
103
|
+
* project has a `linear.projectId`, the daemon polls Linear for delegated-Todo
|
|
104
|
+
* tickets and dispatches them up to the concurrency cap.
|
|
105
|
+
*/
|
|
106
|
+
dispatch?: {
|
|
107
|
+
/** Opt-in: enable auto-dispatch for this project (default: off). */
|
|
108
|
+
enabled?: boolean;
|
|
109
|
+
/** Per-project concurrency cap for auto-dispatched agents. */
|
|
110
|
+
maxAgents?: number;
|
|
111
|
+
/** Optional provider pin: 'rush' | 'codex' | 'factory' | 'host' | … */
|
|
112
|
+
provider?: string;
|
|
113
|
+
/** For provider='host': which machine to dispatch onto (name/device/cap tag). */
|
|
114
|
+
host?: string;
|
|
115
|
+
};
|
|
100
116
|
}
|
|
101
117
|
/** A project name safe to use as a filename: no separators, `..`, or leading dot. */
|
|
102
118
|
export declare function isSafeProjectName(name: string): boolean;
|
|
@@ -116,14 +132,15 @@ export declare function validateProjectDef(raw: unknown, sourceName?: string): P
|
|
|
116
132
|
*/
|
|
117
133
|
export declare function loadProjectDef(name: string): ProjectDef | undefined;
|
|
118
134
|
/**
|
|
119
|
-
* List every defined project, sorted by name.
|
|
120
|
-
*
|
|
121
|
-
*
|
|
135
|
+
* List every defined project, sorted by name. A missing projects directory is
|
|
136
|
+
* the empty state; malformed definitions and filesystem failures stay loud so
|
|
137
|
+
* CLI callers (including Factory) can show the actual error.
|
|
122
138
|
*/
|
|
123
139
|
export declare function listProjectDefs(): ProjectDef[];
|
|
124
140
|
/**
|
|
125
141
|
* Persist a project definition, normalizing `root`/`defaultPath` to home-relative
|
|
126
142
|
* so it stays portable across machines. Creates the projects dir on first write.
|
|
143
|
+
* Writes via temp+rename so readers never see a partial file.
|
|
127
144
|
*/
|
|
128
145
|
export declare function writeProjectDef(def: ProjectDef): string;
|
|
129
146
|
/** Delete a project definition. Returns true if a file was removed. Never touches the repo. */
|
package/dist/lib/projects.js
CHANGED
|
@@ -34,6 +34,7 @@ import { getProjectsDir } from './state.js';
|
|
|
34
34
|
import { safeJoin } from './paths.js';
|
|
35
35
|
import { toHomeRelative, expandLocalHome } from './project-root.js';
|
|
36
36
|
import { resolveProjectKey } from './project-key.js';
|
|
37
|
+
import { atomicWriteFileSync } from './fs-atomic.js';
|
|
37
38
|
/** A project name safe to use as a filename: no separators, `..`, or leading dot. */
|
|
38
39
|
export function isSafeProjectName(name) {
|
|
39
40
|
return (typeof name === 'string' &&
|
|
@@ -155,9 +156,23 @@ export function validateProjectDef(raw, sourceName) {
|
|
|
155
156
|
def.linear.projectId = l.projectId;
|
|
156
157
|
if (typeof l.url === 'string')
|
|
157
158
|
def.linear.url = l.url;
|
|
159
|
+
if (typeof l.name === 'string')
|
|
160
|
+
def.linear.name = l.name;
|
|
158
161
|
}
|
|
159
162
|
if (Array.isArray(o.docs))
|
|
160
163
|
def.docs = o.docs.filter((d) => typeof d === 'string');
|
|
164
|
+
if (o.dispatch && typeof o.dispatch === 'object' && !Array.isArray(o.dispatch)) {
|
|
165
|
+
const d = o.dispatch;
|
|
166
|
+
def.dispatch = {};
|
|
167
|
+
if (d.enabled === true || d.enabled === false)
|
|
168
|
+
def.dispatch.enabled = d.enabled;
|
|
169
|
+
if (typeof d.maxAgents === 'number' && Number.isFinite(d.maxAgents))
|
|
170
|
+
def.dispatch.maxAgents = d.maxAgents;
|
|
171
|
+
if (typeof d.provider === 'string')
|
|
172
|
+
def.dispatch.provider = d.provider;
|
|
173
|
+
if (typeof d.host === 'string')
|
|
174
|
+
def.dispatch.host = d.host;
|
|
175
|
+
}
|
|
161
176
|
return def;
|
|
162
177
|
}
|
|
163
178
|
/**
|
|
@@ -172,15 +187,18 @@ export function loadProjectDef(name) {
|
|
|
172
187
|
try {
|
|
173
188
|
raw = fs.readFileSync(projectDefPath(name), 'utf8');
|
|
174
189
|
}
|
|
175
|
-
catch {
|
|
176
|
-
|
|
190
|
+
catch (error) {
|
|
191
|
+
if (error.code === 'ENOENT') {
|
|
192
|
+
return undefined; // absent — not a defined project
|
|
193
|
+
}
|
|
194
|
+
throw error;
|
|
177
195
|
}
|
|
178
196
|
return validateProjectDef(yaml.parse(raw), name);
|
|
179
197
|
}
|
|
180
198
|
/**
|
|
181
|
-
* List every defined project, sorted by name.
|
|
182
|
-
*
|
|
183
|
-
*
|
|
199
|
+
* List every defined project, sorted by name. A missing projects directory is
|
|
200
|
+
* the empty state; malformed definitions and filesystem failures stay loud so
|
|
201
|
+
* CLI callers (including Factory) can show the actual error.
|
|
184
202
|
*/
|
|
185
203
|
export function listProjectDefs() {
|
|
186
204
|
let files;
|
|
@@ -190,26 +208,24 @@ export function listProjectDefs() {
|
|
|
190
208
|
// loader and silently drop the project. One extension, one code path.
|
|
191
209
|
files = fs.readdirSync(getProjectsDir()).filter((f) => f.endsWith('.yaml'));
|
|
192
210
|
}
|
|
193
|
-
catch {
|
|
194
|
-
|
|
211
|
+
catch (error) {
|
|
212
|
+
if (error.code === 'ENOENT')
|
|
213
|
+
return [];
|
|
214
|
+
throw error;
|
|
195
215
|
}
|
|
196
216
|
const out = [];
|
|
197
217
|
for (const f of files) {
|
|
198
218
|
const name = f.replace(/\.yaml$/, '');
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
out.push(def);
|
|
203
|
-
}
|
|
204
|
-
catch {
|
|
205
|
-
/* malformed — skip in the listing */
|
|
206
|
-
}
|
|
219
|
+
const def = loadProjectDef(name);
|
|
220
|
+
if (def)
|
|
221
|
+
out.push(def);
|
|
207
222
|
}
|
|
208
223
|
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
209
224
|
}
|
|
210
225
|
/**
|
|
211
226
|
* Persist a project definition, normalizing `root`/`defaultPath` to home-relative
|
|
212
227
|
* so it stays portable across machines. Creates the projects dir on first write.
|
|
228
|
+
* Writes via temp+rename so readers never see a partial file.
|
|
213
229
|
*/
|
|
214
230
|
export function writeProjectDef(def) {
|
|
215
231
|
const validated = validateProjectDef(def, def.name);
|
|
@@ -230,7 +246,7 @@ export function writeProjectDef(def) {
|
|
|
230
246
|
const clean = Object.fromEntries(Object.entries(normalized).filter(([, v]) => v !== undefined));
|
|
231
247
|
const target = projectDefPath(def.name);
|
|
232
248
|
fs.mkdirSync(getProjectsDir(), { recursive: true });
|
|
233
|
-
|
|
249
|
+
atomicWriteFileSync(target, yaml.stringify(clean), 'utf8');
|
|
234
250
|
return target;
|
|
235
251
|
}
|
|
236
252
|
/** Delete a project definition. Returns true if a file was removed. Never touches the repo. */
|
|
@@ -239,8 +255,10 @@ export function removeProjectDef(name) {
|
|
|
239
255
|
fs.unlinkSync(projectDefPath(name));
|
|
240
256
|
return true;
|
|
241
257
|
}
|
|
242
|
-
catch {
|
|
243
|
-
|
|
258
|
+
catch (error) {
|
|
259
|
+
if (error.code === 'ENOENT')
|
|
260
|
+
return false;
|
|
261
|
+
throw error;
|
|
244
262
|
}
|
|
245
263
|
}
|
|
246
264
|
/**
|
package/dist/lib/registry.d.ts
CHANGED
|
@@ -85,7 +85,7 @@ export declare function search(query: string, options?: {
|
|
|
85
85
|
}): Promise<RegistrySearchResult[]>;
|
|
86
86
|
/** Parse a package identifier into its type (mcp, skill, git) and name. */
|
|
87
87
|
export declare function parsePackageIdentifier(identifier: string): {
|
|
88
|
-
type: RegistryType | 'git' | 'unknown';
|
|
88
|
+
type: RegistryType | 'git' | 'plugin' | 'unknown';
|
|
89
89
|
name: string;
|
|
90
90
|
};
|
|
91
91
|
/** Resolve a package identifier to an installable package with source metadata. */
|
package/dist/lib/registry.js
CHANGED
|
@@ -387,6 +387,11 @@ export function parsePackageIdentifier(identifier) {
|
|
|
387
387
|
if (identifier.startsWith('skill:')) {
|
|
388
388
|
return { type: 'skill', name: identifier.slice(6) };
|
|
389
389
|
}
|
|
390
|
+
// plugin:name@source | plugin:/path | plugin:gh:user/repo — Phase 5 packaging
|
|
391
|
+
// umbrella. Spec after the prefix is the same grammar as `agents plugins install`.
|
|
392
|
+
if (identifier.startsWith('plugin:')) {
|
|
393
|
+
return { type: 'plugin', name: identifier.slice('plugin:'.length) };
|
|
394
|
+
}
|
|
390
395
|
// gh:user/repo -> git source
|
|
391
396
|
if (identifier.startsWith('gh:')) {
|
|
392
397
|
return { type: 'git', name: identifier };
|
|
@@ -412,6 +417,12 @@ export function parsePackageIdentifier(identifier) {
|
|
|
412
417
|
/** Resolve a package identifier to an installable package with source metadata. */
|
|
413
418
|
export async function resolvePackage(identifier) {
|
|
414
419
|
const parsed = parsePackageIdentifier(identifier);
|
|
420
|
+
if (parsed.type === 'plugin') {
|
|
421
|
+
const spec = parsed.name.trim();
|
|
422
|
+
if (!spec)
|
|
423
|
+
return null;
|
|
424
|
+
return { type: 'plugin', source: spec, pluginSpec: spec };
|
|
425
|
+
}
|
|
415
426
|
if (parsed.type === 'git') {
|
|
416
427
|
return { type: 'git', source: parsed.name };
|
|
417
428
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare function normalizeRoutineNames(names: Iterable<string>): string[];
|
|
2
|
+
/** null means this device has not materialized activation state yet. */
|
|
3
|
+
export declare function enabledRoutineNames(): string[] | null;
|
|
4
|
+
export declare function routineEnabledOnThisDevice(name: string): boolean | null;
|
|
5
|
+
export declare function replaceEnabledRoutines(names: Iterable<string>): string[];
|
|
6
|
+
/**
|
|
7
|
+
* Add or remove one routine on this machine. `legacyEnabledNames` seeds the
|
|
8
|
+
* manifest the first time an upgraded host changes activation, preserving every
|
|
9
|
+
* other routine that was effectively enabled under the old definition fields.
|
|
10
|
+
*/
|
|
11
|
+
export declare function setRoutineEnabledOnThisDevice(name: string, enabled: boolean, legacyEnabledNames?: Iterable<string>): string[];
|
|
12
|
+
/** Read-only fleet view from synced device documents. Never writes a peer file. */
|
|
13
|
+
export declare function devicesWithRoutineEnabled(name: string): string[];
|
|
14
|
+
export declare function currentRoutineDevice(): string;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** Device-scoped routine activation.
|
|
2
|
+
*
|
|
3
|
+
* Definitions say what a routine does. This module owns the independent answer
|
|
4
|
+
* to whether THIS device runs it: membership in the top-level `routines:` list
|
|
5
|
+
* of `~/.agents/devices/<machine>/agents.yaml`.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import * as yaml from 'yaml';
|
|
10
|
+
import { machineId } from './machine-id.js';
|
|
11
|
+
import { getUserAgentsDir, readMeta, updateMeta } from './state.js';
|
|
12
|
+
export function normalizeRoutineNames(names) {
|
|
13
|
+
return [...new Set([...names].map((name) => name.trim()).filter(Boolean))].sort();
|
|
14
|
+
}
|
|
15
|
+
/** null means this device has not materialized activation state yet. */
|
|
16
|
+
export function enabledRoutineNames() {
|
|
17
|
+
const names = readMeta().deviceRoutines;
|
|
18
|
+
return Array.isArray(names) ? normalizeRoutineNames(names) : null;
|
|
19
|
+
}
|
|
20
|
+
export function routineEnabledOnThisDevice(name) {
|
|
21
|
+
const names = enabledRoutineNames();
|
|
22
|
+
return names === null ? null : names.includes(name);
|
|
23
|
+
}
|
|
24
|
+
export function replaceEnabledRoutines(names) {
|
|
25
|
+
const normalized = normalizeRoutineNames(names);
|
|
26
|
+
updateMeta((meta) => ({ ...meta, deviceRoutines: normalized }));
|
|
27
|
+
return normalized;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Add or remove one routine on this machine. `legacyEnabledNames` seeds the
|
|
31
|
+
* manifest the first time an upgraded host changes activation, preserving every
|
|
32
|
+
* other routine that was effectively enabled under the old definition fields.
|
|
33
|
+
*/
|
|
34
|
+
export function setRoutineEnabledOnThisDevice(name, enabled, legacyEnabledNames = []) {
|
|
35
|
+
const current = enabledRoutineNames() ?? normalizeRoutineNames(legacyEnabledNames);
|
|
36
|
+
const next = new Set(current);
|
|
37
|
+
if (enabled)
|
|
38
|
+
next.add(name);
|
|
39
|
+
else
|
|
40
|
+
next.delete(name);
|
|
41
|
+
return replaceEnabledRoutines(next);
|
|
42
|
+
}
|
|
43
|
+
/** Read-only fleet view from synced device documents. Never writes a peer file. */
|
|
44
|
+
export function devicesWithRoutineEnabled(name) {
|
|
45
|
+
const devicesDir = path.join(getUserAgentsDir(), 'devices');
|
|
46
|
+
if (!fs.existsSync(devicesDir))
|
|
47
|
+
return [];
|
|
48
|
+
const devices = [];
|
|
49
|
+
for (const entry of fs.readdirSync(devicesDir, { withFileTypes: true })) {
|
|
50
|
+
if (!entry.isDirectory())
|
|
51
|
+
continue;
|
|
52
|
+
const file = path.join(devicesDir, entry.name, 'agents.yaml');
|
|
53
|
+
if (!fs.existsSync(file))
|
|
54
|
+
continue;
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = yaml.parse(fs.readFileSync(file, 'utf-8'));
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
throw new Error(`Device config corrupted at ${file}: ${err.message}`);
|
|
61
|
+
}
|
|
62
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
63
|
+
throw new Error(`Device config corrupted at ${file}: expected a YAML map.`);
|
|
64
|
+
}
|
|
65
|
+
const routines = parsed.routines;
|
|
66
|
+
if (routines === undefined)
|
|
67
|
+
continue;
|
|
68
|
+
if (!Array.isArray(routines) || routines.some((routine) => typeof routine !== 'string')) {
|
|
69
|
+
throw new Error(`Device config corrupted at ${file}: routines must be a string list.`);
|
|
70
|
+
}
|
|
71
|
+
if (routines.includes(name))
|
|
72
|
+
devices.push(entry.name);
|
|
73
|
+
}
|
|
74
|
+
return devices.sort();
|
|
75
|
+
}
|
|
76
|
+
export function currentRoutineDevice() {
|
|
77
|
+
return machineId();
|
|
78
|
+
}
|
|
@@ -67,7 +67,7 @@ export interface SyncProjectResult {
|
|
|
67
67
|
* - Overwrites user copies that already carry matching `source.projectPath`
|
|
68
68
|
* - Never clobbers a hand-authored user routine (no source / different source)
|
|
69
69
|
* - Removes user copies from this project whose YAML disappeared
|
|
70
|
-
* -
|
|
70
|
+
* - Activates materialized routines on this device without mutating definitions
|
|
71
71
|
*/
|
|
72
72
|
export declare function syncProjectRoutines(projectRoot: string): SyncProjectResult;
|
|
73
73
|
export interface SyncAllResult {
|
|
@@ -14,9 +14,8 @@ import * as path from 'path';
|
|
|
14
14
|
import * as yaml from 'yaml';
|
|
15
15
|
import { execFileSync } from 'child_process';
|
|
16
16
|
import { getProjectAgentsDir, getProjectRoutinesDir, readMeta, updateMeta, ensureAgentsDir, } from './state.js';
|
|
17
|
-
import { readJob, writeJob, deleteJob, listJobs, validateJob,
|
|
17
|
+
import { readJob, writeJob, setJobEnabled, deleteJob, listJobs, validateJob, } from './routines.js';
|
|
18
18
|
import { parseOwnerRepoFromRemote } from './registry.js';
|
|
19
|
-
import { machineId } from './machine-id.js';
|
|
20
19
|
import { isSafeSegmentName } from './paths.js';
|
|
21
20
|
/** Expand `~/…` and resolve to an absolute path. */
|
|
22
21
|
export function expandProjectPath(p) {
|
|
@@ -221,7 +220,7 @@ function readProjectJobFile(filePath) {
|
|
|
221
220
|
* - Overwrites user copies that already carry matching `source.projectPath`
|
|
222
221
|
* - Never clobbers a hand-authored user routine (no source / different source)
|
|
223
222
|
* - Removes user copies from this project whose YAML disappeared
|
|
224
|
-
* -
|
|
223
|
+
* - Activates materialized routines on this device without mutating definitions
|
|
225
224
|
*/
|
|
226
225
|
export function syncProjectRoutines(projectRoot) {
|
|
227
226
|
ensureAgentsDir();
|
|
@@ -276,12 +275,6 @@ export function syncProjectRoutines(projectRoot) {
|
|
|
276
275
|
if (existing.createdAt)
|
|
277
276
|
job.createdAt = existing.createdAt;
|
|
278
277
|
}
|
|
279
|
-
// Placement that leaves the firing machine must pin devices to avoid
|
|
280
|
-
// every fleet daemon dispatching once.
|
|
281
|
-
const strategy = resolveHostStrategy(job);
|
|
282
|
-
if (placementRequiresFiringPin(strategy) && (!job.devices || job.devices.length === 0)) {
|
|
283
|
-
job.devices = [machineId()];
|
|
284
|
-
}
|
|
285
278
|
// Surface repo for list/display when project has a GitHub origin.
|
|
286
279
|
if (git.repo && !job.repo)
|
|
287
280
|
job.repo = git.repo;
|
|
@@ -294,6 +287,7 @@ export function syncProjectRoutines(projectRoot) {
|
|
|
294
287
|
}
|
|
295
288
|
try {
|
|
296
289
|
writeJob(job);
|
|
290
|
+
setJobEnabled(job.name, job.enabled);
|
|
297
291
|
result.synced.push(file.name);
|
|
298
292
|
}
|
|
299
293
|
catch (err) {
|
package/dist/lib/routines.d.ts
CHANGED
|
@@ -294,7 +294,7 @@ export declare function finalizeRunMeta(meta: RunMeta, status: RunMeta['status']
|
|
|
294
294
|
* `yosemite-s0` all agree. Every fire path (cron scheduler, webhook,
|
|
295
295
|
* catchup/overdue, manual run) gates on this.
|
|
296
296
|
*/
|
|
297
|
-
export declare function jobRunsOnThisDevice(config: Pick<JobConfig, 'devices'>): boolean;
|
|
297
|
+
export declare function jobRunsOnThisDevice(config: Pick<JobConfig, 'name' | 'devices'>): boolean;
|
|
298
298
|
/**
|
|
299
299
|
* The ONE device that owns a routine — the single daemon allowed to fire it.
|
|
300
300
|
*
|
|
@@ -423,6 +423,8 @@ export declare function serializeJob(output: Record<string, unknown>, existingTe
|
|
|
423
423
|
export declare function deleteJob(name: string): boolean;
|
|
424
424
|
/** Enable or disable a job by name. */
|
|
425
425
|
export declare function setJobEnabled(name: string, enabled: boolean): void;
|
|
426
|
+
/** Materialize legacy definition state into this device's activation manifest. */
|
|
427
|
+
export declare function migrateLegacyRoutineActivation(): boolean;
|
|
426
428
|
/** Validate a partial job config, returning a list of human-readable errors. */
|
|
427
429
|
export declare function validateJob(config: Partial<JobConfig>): string[];
|
|
428
430
|
/** Validate a job trigger block, returning a list of human-readable errors. */
|