@phnx-labs/agents-cli 1.20.52 → 1.20.53

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.
Files changed (107) hide show
  1. package/CHANGELOG.md +57 -2
  2. package/README.md +12 -0
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/cloud.d.ts +3 -0
  5. package/dist/commands/cloud.js +2 -1
  6. package/dist/commands/exec.js +65 -1
  7. package/dist/commands/feed.d.ts +29 -0
  8. package/dist/commands/feed.js +237 -32
  9. package/dist/commands/memory.d.ts +9 -0
  10. package/dist/commands/memory.js +164 -0
  11. package/dist/commands/message.d.ts +11 -6
  12. package/dist/commands/message.js +140 -5
  13. package/dist/commands/routines.js +12 -0
  14. package/dist/commands/secrets-migrate.d.ts +2 -1
  15. package/dist/commands/secrets-migrate.js +88 -13
  16. package/dist/commands/secrets.js +4 -1
  17. package/dist/commands/sessions.js +10 -1
  18. package/dist/commands/worktree.js +4 -2
  19. package/dist/index.js +16 -21
  20. package/dist/lib/agents.js +249 -17
  21. package/dist/lib/answer-router.d.ts +75 -0
  22. package/dist/lib/answer-router.js +149 -0
  23. package/dist/lib/ask-classifier.d.ts +71 -0
  24. package/dist/lib/ask-classifier.js +197 -0
  25. package/dist/lib/cloud/antigravity.d.ts +0 -2
  26. package/dist/lib/cloud/antigravity.js +2 -17
  27. package/dist/lib/cloud/codex.js +3 -18
  28. package/dist/lib/cloud/rush.js +3 -15
  29. package/dist/lib/cloud/stream.js +2 -0
  30. package/dist/lib/cloud/types.d.ts +21 -0
  31. package/dist/lib/cloud/types.js +81 -0
  32. package/dist/lib/crabbox/cli.d.ts +1 -1
  33. package/dist/lib/crabbox/cli.js +12 -2
  34. package/dist/lib/crabbox/lease.d.ts +13 -0
  35. package/dist/lib/crabbox/lease.js +11 -2
  36. package/dist/lib/crabbox/progress.d.ts +62 -0
  37. package/dist/lib/crabbox/progress.js +129 -0
  38. package/dist/lib/events.js +4 -1
  39. package/dist/lib/exec.js +19 -1
  40. package/dist/lib/feed-outcome.d.ts +101 -0
  41. package/dist/lib/feed-outcome.js +244 -0
  42. package/dist/lib/feed-policy.d.ts +30 -0
  43. package/dist/lib/feed-policy.js +133 -0
  44. package/dist/lib/feed.d.ts +127 -3
  45. package/dist/lib/feed.js +416 -40
  46. package/dist/lib/git.d.ts +17 -1
  47. package/dist/lib/git.js +20 -1
  48. package/dist/lib/hooks.js +522 -12
  49. package/dist/lib/hosts/passthrough.d.ts +3 -3
  50. package/dist/lib/hosts/passthrough.js +3 -4
  51. package/dist/lib/mailbox-gc.d.ts +22 -0
  52. package/dist/lib/mailbox-gc.js +161 -0
  53. package/dist/lib/mailbox.d.ts +26 -2
  54. package/dist/lib/mailbox.js +80 -5
  55. package/dist/lib/mcp.js +82 -0
  56. package/dist/lib/memory.d.ts +55 -0
  57. package/dist/lib/memory.js +274 -0
  58. package/dist/lib/notify.d.ts +16 -0
  59. package/dist/lib/notify.js +61 -0
  60. package/dist/lib/operator.d.ts +26 -0
  61. package/dist/lib/operator.js +107 -0
  62. package/dist/lib/plugins.d.ts +35 -0
  63. package/dist/lib/plugins.js +217 -0
  64. package/dist/lib/remote-agents-json.d.ts +14 -0
  65. package/dist/lib/remote-agents-json.js +94 -0
  66. package/dist/lib/resources/mcp.js +44 -0
  67. package/dist/lib/resources/memory.d.ts +15 -0
  68. package/dist/lib/resources/memory.js +46 -0
  69. package/dist/lib/resources/types.d.ts +2 -2
  70. package/dist/lib/runner.d.ts +43 -0
  71. package/dist/lib/runner.js +323 -74
  72. package/dist/lib/sandbox.js +6 -0
  73. package/dist/lib/secrets/bundles.js +38 -13
  74. package/dist/lib/secrets/icloud-import.d.ts +12 -3
  75. package/dist/lib/secrets/icloud-import.js +37 -7
  76. package/dist/lib/secrets/index.d.ts +106 -2
  77. package/dist/lib/secrets/index.js +603 -28
  78. package/dist/lib/session/active.d.ts +18 -0
  79. package/dist/lib/session/active.js +47 -17
  80. package/dist/lib/session/db.d.ts +9 -1
  81. package/dist/lib/session/db.js +18 -3
  82. package/dist/lib/session/discover.d.ts +13 -0
  83. package/dist/lib/session/discover.js +31 -0
  84. package/dist/lib/session/parse.d.ts +8 -0
  85. package/dist/lib/session/parse.js +42 -21
  86. package/dist/lib/session/remote-active.js +8 -89
  87. package/dist/lib/session/state.d.ts +11 -0
  88. package/dist/lib/session/state.js +37 -0
  89. package/dist/lib/session/tail.d.ts +23 -4
  90. package/dist/lib/session/tail.js +34 -16
  91. package/dist/lib/session/throughput.d.ts +30 -0
  92. package/dist/lib/session/throughput.js +86 -0
  93. package/dist/lib/shim-heal.d.ts +12 -3
  94. package/dist/lib/shim-heal.js +12 -6
  95. package/dist/lib/staleness/detectors/subagents.js +57 -3
  96. package/dist/lib/staleness/writers/hooks.js +7 -3
  97. package/dist/lib/staleness/writers/subagents.js +37 -6
  98. package/dist/lib/startup/command-registry.d.ts +1 -0
  99. package/dist/lib/startup/command-registry.js +2 -0
  100. package/dist/lib/subagents.d.ts +52 -0
  101. package/dist/lib/subagents.js +315 -12
  102. package/dist/lib/teams/worktree.d.ts +8 -0
  103. package/dist/lib/teams/worktree.js +8 -0
  104. package/dist/lib/types.d.ts +10 -2
  105. package/dist/lib/versions.js +38 -48
  106. package/package.json +4 -3
  107. package/scripts/postinstall.js +61 -1
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Canonical agent memory resource — accumulated facts/preferences/knowledge
3
+ * distinct from `rules` (instructions / AGENTS.md persona).
4
+ *
5
+ * Layout (project > user > system layering):
6
+ * ~/.agents/memory/MEMORY.md always-read index
7
+ * ~/.agents/memory/<slug>.md individual facts
8
+ * ~/.agents/.system/memory/ system layer
9
+ * <project>/.agents/memory/ project layer
10
+ *
11
+ * Sync fans out into each capable agent's version home under an agent-specific
12
+ * target dir (see memoryTargetDir).
13
+ */
14
+ import * as fs from 'fs';
15
+ import * as path from 'path';
16
+ import { getUserAgentsDir, getSystemAgentsDir, getProjectAgentsDir, ensureAgentsDir, } from './state.js';
17
+ import { agentConfigDirName } from './agents.js';
18
+ import { supports } from './capabilities.js';
19
+ /** User-layer memory root (~/.agents/memory/). */
20
+ export function getUserMemoryDir() {
21
+ return path.join(getUserAgentsDir(), 'memory');
22
+ }
23
+ /** System-layer memory root (~/.agents/.system/memory/). */
24
+ export function getSystemMemoryDir() {
25
+ return path.join(getSystemAgentsDir(), 'memory');
26
+ }
27
+ /** Project-layer memory root when a project agents dir exists. */
28
+ export function getProjectMemoryDir(cwd = process.cwd()) {
29
+ const project = getProjectAgentsDir(cwd);
30
+ return project ? path.join(project, 'memory') : null;
31
+ }
32
+ /** Ensure the user memory dir exists (creates MEMORY.md index if missing). */
33
+ export function ensureUserMemoryDir() {
34
+ ensureAgentsDir();
35
+ const dir = getUserMemoryDir();
36
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
37
+ const index = path.join(dir, 'MEMORY.md');
38
+ if (!fs.existsSync(index)) {
39
+ fs.writeFileSync(index, [
40
+ '# Memory index',
41
+ '',
42
+ 'Always-read summary of accumulated agent memory. Individual facts live',
43
+ 'as sibling `*.md` files. Managed by `agents memory`.',
44
+ '',
45
+ ].join('\n'), 'utf-8');
46
+ }
47
+ return dir;
48
+ }
49
+ function isFactFile(name) {
50
+ return name.endsWith('.md') && name.toLowerCase() !== 'memory.md';
51
+ }
52
+ function slugify(name) {
53
+ return name
54
+ .trim()
55
+ .toLowerCase()
56
+ .replace(/[^a-z0-9._-]+/g, '-')
57
+ .replace(/^-+|-+$/g, '')
58
+ .slice(0, 64) || 'fact';
59
+ }
60
+ function summarize(content) {
61
+ for (const line of content.split('\n')) {
62
+ const t = line.trim();
63
+ if (!t || t.startsWith('#'))
64
+ continue;
65
+ return t.length > 80 ? t.slice(0, 77) + '...' : t;
66
+ }
67
+ return '(empty)';
68
+ }
69
+ /** Layer dirs highest-priority first. */
70
+ export function getMemoryLayerDirs(cwd = process.cwd()) {
71
+ const out = [];
72
+ const project = getProjectMemoryDir(cwd);
73
+ if (project && fs.existsSync(project))
74
+ out.push({ layer: 'project', dir: project });
75
+ const user = getUserMemoryDir();
76
+ if (fs.existsSync(user))
77
+ out.push({ layer: 'user', dir: user });
78
+ const system = getSystemMemoryDir();
79
+ if (fs.existsSync(system))
80
+ out.push({ layer: 'system', dir: system });
81
+ return out;
82
+ }
83
+ /** List memory facts with project > user > system override on name. */
84
+ export function listMemoryFacts(cwd = process.cwd()) {
85
+ const seen = new Set();
86
+ const results = [];
87
+ for (const { layer, dir } of getMemoryLayerDirs(cwd)) {
88
+ let entries = [];
89
+ try {
90
+ entries = fs.readdirSync(dir);
91
+ }
92
+ catch {
93
+ continue;
94
+ }
95
+ for (const file of entries) {
96
+ if (!isFactFile(file))
97
+ continue;
98
+ const name = file.replace(/\.md$/i, '');
99
+ if (seen.has(name))
100
+ continue;
101
+ seen.add(name);
102
+ const filePath = path.join(dir, file);
103
+ let content = '';
104
+ try {
105
+ content = fs.readFileSync(filePath, 'utf-8');
106
+ }
107
+ catch {
108
+ continue;
109
+ }
110
+ results.push({ name, path: filePath, layer, summary: summarize(content) });
111
+ }
112
+ }
113
+ return results.sort((a, b) => a.name.localeCompare(b.name));
114
+ }
115
+ /** Read one fact by name (winning layer). */
116
+ export function readMemoryFact(name, cwd = process.cwd()) {
117
+ const slug = slugify(name);
118
+ for (const { layer, dir } of getMemoryLayerDirs(cwd)) {
119
+ const filePath = path.join(dir, `${slug}.md`);
120
+ if (!fs.existsSync(filePath))
121
+ continue;
122
+ let content = '';
123
+ try {
124
+ content = fs.readFileSync(filePath, 'utf-8');
125
+ }
126
+ catch {
127
+ continue;
128
+ }
129
+ return { name: slug, path: filePath, layer, summary: summarize(content) };
130
+ }
131
+ return null;
132
+ }
133
+ /** Write a fact into the user layer. Returns the absolute path. */
134
+ export function addMemoryFact(name, body) {
135
+ const dir = ensureUserMemoryDir();
136
+ const slug = slugify(name);
137
+ const filePath = path.join(dir, `${slug}.md`);
138
+ const content = body.trimStart().startsWith('#')
139
+ ? body.endsWith('\n') ? body : body + '\n'
140
+ : `# ${slug}\n\n${body.trim()}\n`;
141
+ fs.writeFileSync(filePath, content, 'utf-8');
142
+ rebuildMemoryIndex(dir);
143
+ return filePath;
144
+ }
145
+ /** Remove a fact from the user layer. Returns true if a file was deleted. */
146
+ export function removeMemoryFact(name) {
147
+ const dir = getUserMemoryDir();
148
+ const slug = slugify(name);
149
+ const filePath = path.join(dir, `${slug}.md`);
150
+ if (!fs.existsSync(filePath))
151
+ return false;
152
+ fs.unlinkSync(filePath);
153
+ if (fs.existsSync(dir))
154
+ rebuildMemoryIndex(dir);
155
+ return true;
156
+ }
157
+ /** Rebuild MEMORY.md index from sibling fact files in a single layer dir. */
158
+ export function rebuildMemoryIndex(dir) {
159
+ let facts = [];
160
+ try {
161
+ facts = fs.readdirSync(dir).filter(isFactFile).sort();
162
+ }
163
+ catch {
164
+ return;
165
+ }
166
+ const lines = [
167
+ '# Memory index',
168
+ '',
169
+ 'Always-read summary of accumulated agent memory. Managed by `agents memory`.',
170
+ '',
171
+ ];
172
+ if (facts.length === 0) {
173
+ lines.push('_No facts yet. Add one with `agents memory add <name> --body "..."`._', '');
174
+ }
175
+ else {
176
+ for (const file of facts) {
177
+ const name = file.replace(/\.md$/i, '');
178
+ let summary = '';
179
+ try {
180
+ summary = summarize(fs.readFileSync(path.join(dir, file), 'utf-8'));
181
+ }
182
+ catch {
183
+ summary = '';
184
+ }
185
+ lines.push(`- **${name}** — ${summary}`);
186
+ }
187
+ lines.push('');
188
+ }
189
+ fs.writeFileSync(path.join(dir, 'MEMORY.md'), lines.join('\n'), 'utf-8');
190
+ }
191
+ /**
192
+ * Per-agent target directory (relative to version home) for synced memory.
193
+ * Claude/Codex/OpenClaw/Grok get native-ish paths; others get a generic memory/.
194
+ */
195
+ export function memoryTargetDir(agent) {
196
+ switch (agent) {
197
+ case 'claude':
198
+ return path.join(agentConfigDirName(agent), 'memory');
199
+ case 'codex':
200
+ return path.join(agentConfigDirName(agent), 'memories');
201
+ case 'openclaw':
202
+ case 'grok':
203
+ return 'memory';
204
+ default:
205
+ return path.join(agentConfigDirName(agent), 'memory');
206
+ }
207
+ }
208
+ /** Copy canonical layered memory into one version home. Returns fact names written. */
209
+ export function syncMemoryToVersionHome(agent, versionHome, cwd = process.cwd()) {
210
+ if (!supports(agent, 'memory').ok)
211
+ return [];
212
+ const facts = listMemoryFacts(cwd);
213
+ const targetRel = memoryTargetDir(agent);
214
+ const targetDir = path.join(versionHome, targetRel);
215
+ fs.mkdirSync(targetDir, { recursive: true });
216
+ // Managed manifest tracks which fact files *we* wrote. On sync we only
217
+ // remove previously-managed names that are no longer in the canonical set —
218
+ // never wipe every .md (user-authored native memory must survive).
219
+ const managedManifestPath = path.join(targetDir, '.agents-cli-memory.json');
220
+ let previouslyManaged = [];
221
+ try {
222
+ const raw = JSON.parse(fs.readFileSync(managedManifestPath, 'utf-8'));
223
+ if (Array.isArray(raw.facts)) {
224
+ previouslyManaged = raw.facts.filter((f) => typeof f === 'string');
225
+ }
226
+ }
227
+ catch { /* missing or corrupt → treat as first managed sync */ }
228
+ const desiredNames = new Set(facts.map((f) => f.name));
229
+ const managedThisSync = new Set(desiredNames);
230
+ for (const name of previouslyManaged) {
231
+ if (desiredNames.has(name))
232
+ continue;
233
+ if (name === 'MEMORY')
234
+ continue; // index rewritten below
235
+ try {
236
+ fs.unlinkSync(path.join(targetDir, `${name}.md`));
237
+ }
238
+ catch { /* ignore */ }
239
+ }
240
+ const written = [];
241
+ for (const fact of facts) {
242
+ const dest = path.join(targetDir, `${fact.name}.md`);
243
+ try {
244
+ fs.copyFileSync(fact.path, dest);
245
+ written.push(fact.name);
246
+ }
247
+ catch { /* skip unreadable */ }
248
+ }
249
+ // Always write an index from the winning layers.
250
+ const indexSrc = (() => {
251
+ for (const { dir } of getMemoryLayerDirs(cwd)) {
252
+ const p = path.join(dir, 'MEMORY.md');
253
+ if (fs.existsSync(p))
254
+ return p;
255
+ }
256
+ return null;
257
+ })();
258
+ if (indexSrc) {
259
+ try {
260
+ fs.copyFileSync(indexSrc, path.join(targetDir, 'MEMORY.md'));
261
+ managedThisSync.add('MEMORY');
262
+ }
263
+ catch { /* ignore */ }
264
+ }
265
+ else {
266
+ rebuildMemoryIndex(targetDir);
267
+ managedThisSync.add('MEMORY');
268
+ }
269
+ try {
270
+ fs.writeFileSync(managedManifestPath, JSON.stringify({ facts: [...managedThisSync].sort() }, null, 2) + '\n', 'utf-8');
271
+ }
272
+ catch { /* best-effort */ }
273
+ return written;
274
+ }
@@ -0,0 +1,16 @@
1
+ import type { OpenBlock } from './feed.js';
2
+ export interface NotifyOptions {
3
+ channel?: string;
4
+ account?: string;
5
+ /** OpenClaw destination (Telegram chat id). Defaults to Muqsit's chat. */
6
+ target?: string;
7
+ dryRun?: boolean;
8
+ }
9
+ export interface NotifyResult {
10
+ ok: boolean;
11
+ skipped?: boolean;
12
+ error?: string;
13
+ }
14
+ /** Build openclaw argv for urgent notify (exported for tests). */
15
+ export declare function buildOpenClawNotifyArgs(text: string, options?: Pick<NotifyOptions, 'channel' | 'account' | 'target'>): string[];
16
+ export declare function notifyUrgentBlock(block: OpenBlock, options?: NotifyOptions): Promise<NotifyResult>;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Urgent-block phone notifier.
3
+ *
4
+ * Reuses the OpenClaw Telegram gateway on the local mac-mini (Jeff/`default` bot)
5
+ * instead of raw bot tokens. Notifies once per block (tracked by `notifiedAt`).
6
+ * Best-effort: any openclaw failure is surfaced as a warning, not a hard error,
7
+ * so a notification hiccup never blocks the agent.
8
+ */
9
+ import { execFile } from 'child_process';
10
+ import { promisify } from 'util';
11
+ const execFileAsync = promisify(execFile);
12
+ function formatBlock(block) {
13
+ const q = block.questions[0];
14
+ const header = q?.header ? `[${q.header}] ` : '';
15
+ const text = q?.text ?? 'Agent needs input';
16
+ const host = block.host ? ` on ${block.host}` : '';
17
+ const cls = block.blockClass ?? 'approval';
18
+ const cost = block.costOfDelay ?? 'low';
19
+ return `🚨 ${cls.toUpperCase()}${host} — ${header}${text} (cost: ${cost}, id: ${block.blockId})`;
20
+ }
21
+ /** Build openclaw argv for urgent notify (exported for tests). */
22
+ export function buildOpenClawNotifyArgs(text, options = {}) {
23
+ const channel = options.channel ?? 'telegram';
24
+ const account = options.account ?? 'default';
25
+ const target = options.target ?? '6078999250';
26
+ return [
27
+ 'message',
28
+ 'send',
29
+ '--channel',
30
+ channel,
31
+ '--account',
32
+ account,
33
+ '--target',
34
+ target,
35
+ '--message',
36
+ text,
37
+ ];
38
+ }
39
+ export async function notifyUrgentBlock(block, options = {}) {
40
+ if (block.notifiedAt) {
41
+ return { ok: true, skipped: true };
42
+ }
43
+ if (options.dryRun) {
44
+ return { ok: true, skipped: true };
45
+ }
46
+ // Check that openclaw is installed.
47
+ try {
48
+ await execFileAsync('which', ['openclaw']);
49
+ }
50
+ catch {
51
+ return { ok: false, error: 'openclaw CLI not found on PATH' };
52
+ }
53
+ const text = formatBlock(block);
54
+ try {
55
+ await execFileAsync('openclaw', buildOpenClawNotifyArgs(text, options));
56
+ return { ok: true };
57
+ }
58
+ catch (err) {
59
+ return { ok: false, error: err.message };
60
+ }
61
+ }
@@ -0,0 +1,26 @@
1
+ export interface Operator {
2
+ id: string;
3
+ name?: string;
4
+ admin?: boolean;
5
+ can?: string[];
6
+ }
7
+ export interface OperatorRegistry {
8
+ operators: Record<string, Operator>;
9
+ }
10
+ export declare function getOperatorsPath(root?: string): string;
11
+ export declare function loadOperators(root?: string): OperatorRegistry;
12
+ export declare function getOperator(id: string, root?: string): Operator | undefined;
13
+ export declare function isKnownOperator(id: string, root?: string): boolean;
14
+ export declare function isAdmin(id: string, root?: string): boolean;
15
+ export declare function canPerform(id: string, action: string, root?: string): boolean;
16
+ /** High-consequence blocks require a known operator with explicit merge/deploy/admin rights. */
17
+ export declare function isHighConsequenceAllowed(blockConsequence: string | undefined, operatorId: string, root?: string): boolean;
18
+ /**
19
+ * Prove operator identity for high-consequence answers.
20
+ *
21
+ * Knowing an id listed in operators.yaml is not authentication — any same-user
22
+ * process can pass `--as muqsit`. Require the process environment to claim the
23
+ * same id via AGENTS_OPERATOR_ID (typically injected by `agents secrets` /
24
+ * the human's launch context).
25
+ */
26
+ export declare function verifyOperatorIdentity(claimedId: string | undefined, root?: string): boolean;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Operator identity registry for multi-human feed controls.
3
+ *
4
+ * The mailbox `from` label is caller-supplied and unverified (same-user-writable).
5
+ * For routine answers that's fine; for high-consequence blocks (merge/deploy/admin)
6
+ * we need a verified operator identity. This module loads a local registry of
7
+ * operators from ~/.agents/operators.yaml and provides the authz check used by
8
+ * `recordAnswer` and `agents message --as`.
9
+ *
10
+ * Registry format (YAML):
11
+ * operators:
12
+ * muqsit:
13
+ * name: Muqsit
14
+ * admin: true
15
+ * bisma:
16
+ * name: Bisma
17
+ * can:
18
+ * - merge
19
+ * - deploy
20
+ *
21
+ * For this release identity is proven by knowing the operator id (local registry
22
+ * membership). A future release can add public-key/totp challenge without changing
23
+ * the call sites.
24
+ */
25
+ import * as fs from 'fs';
26
+ import * as path from 'path';
27
+ import * as yaml from 'yaml';
28
+ import { getUserAgentsDir } from './state.js';
29
+ const OPERATORS_FILE = 'operators.yaml';
30
+ export function getOperatorsPath(root) {
31
+ return path.join(root ?? getUserAgentsDir(), OPERATORS_FILE);
32
+ }
33
+ export function loadOperators(root) {
34
+ const file = getOperatorsPath(root);
35
+ try {
36
+ const raw = fs.readFileSync(file, 'utf-8');
37
+ const parsed = yaml.parse(raw);
38
+ if (parsed && typeof parsed === 'object' && 'operators' in parsed) {
39
+ const ops = parsed.operators;
40
+ const operators = {};
41
+ for (const [id, v] of Object.entries(ops)) {
42
+ if (v && typeof v === 'object') {
43
+ const entry = v;
44
+ operators[id] = {
45
+ id,
46
+ name: typeof entry.name === 'string' ? entry.name : undefined,
47
+ admin: entry.admin === true,
48
+ can: Array.isArray(entry.can)
49
+ ? entry.can.filter((c) => typeof c === 'string')
50
+ : undefined,
51
+ };
52
+ }
53
+ }
54
+ return { operators };
55
+ }
56
+ }
57
+ catch {
58
+ // missing or malformed -> empty registry
59
+ }
60
+ return { operators: {} };
61
+ }
62
+ export function getOperator(id, root) {
63
+ return loadOperators(root).operators[id];
64
+ }
65
+ export function isKnownOperator(id, root) {
66
+ return getOperator(id, root) !== undefined;
67
+ }
68
+ export function isAdmin(id, root) {
69
+ const op = getOperator(id, root);
70
+ return op?.admin === true;
71
+ }
72
+ export function canPerform(id, action, root) {
73
+ const op = getOperator(id, root);
74
+ if (!op)
75
+ return false;
76
+ if (op.admin)
77
+ return true;
78
+ return op.can?.includes(action) ?? false;
79
+ }
80
+ /** High-consequence blocks require a known operator with explicit merge/deploy/admin rights. */
81
+ export function isHighConsequenceAllowed(blockConsequence, operatorId, root) {
82
+ if (!blockConsequence || blockConsequence === 'normal')
83
+ return true;
84
+ if (!isKnownOperator(operatorId, root))
85
+ return false;
86
+ if (isAdmin(operatorId, root))
87
+ return true;
88
+ return canPerform(operatorId, blockConsequence, root);
89
+ }
90
+ /**
91
+ * Prove operator identity for high-consequence answers.
92
+ *
93
+ * Knowing an id listed in operators.yaml is not authentication — any same-user
94
+ * process can pass `--as muqsit`. Require the process environment to claim the
95
+ * same id via AGENTS_OPERATOR_ID (typically injected by `agents secrets` /
96
+ * the human's launch context).
97
+ */
98
+ export function verifyOperatorIdentity(claimedId, root) {
99
+ if (!claimedId)
100
+ return false;
101
+ if (!isKnownOperator(claimedId, root))
102
+ return false;
103
+ const envId = process.env.AGENTS_OPERATOR_ID?.trim();
104
+ if (!envId)
105
+ return false;
106
+ return envId === claimedId;
107
+ }
@@ -79,6 +79,8 @@ export declare function getPlugin(name: string): DiscoveredPlugin | null;
79
79
  * Otherwise defaults to all plugin-capable agents.
80
80
  */
81
81
  export declare function pluginSupportsAgent(plugin: DiscoveredPlugin, agent: AgentId): boolean;
82
+ /** Fact basenames (no .md) from a plugin's memory/ directory. */
83
+ export declare function discoverPluginMemory(pluginRoot: string): string[];
82
84
  /**
83
85
  * The lifecycle events a plugin hooks into, read from hooks/hooks.json.
84
86
  *
@@ -168,6 +170,39 @@ export declare function syncPluginToVersion(plugin: DiscoveredPlugin, agent: Age
168
170
  mcp: boolean;
169
171
  settings: boolean;
170
172
  };
173
+ /**
174
+ * OpenCode loads JS/TS modules from `$HOME/.config/opencode/plugins/` (global)
175
+ * and `<project>/.opencode/plugins/` (project). Under agents-cli version
176
+ * isolation HOME is the version home, so we write:
177
+ * {versionHome}/.config/opencode/plugins/
178
+ *
179
+ * Claude-style marketplace plugins are NOT auto-converted; we install modules
180
+ * from (in order):
181
+ * 1. pluginRoot/opencode/*.{ts,js,mjs,cjs}
182
+ * 2. pluginRoot/plugins/*.{ts,js,mjs,cjs}
183
+ * 3. pluginRoot/*.{ts,js,mjs,cjs} (excluding *.test.* / *.spec.*)
184
+ * If none exist, install still succeeds by writing a marker + copying any
185
+ * package.json so empty plugins don't break sync; opencode simply has nothing
186
+ * to load until a real module appears.
187
+ */
188
+ export declare function openCodePluginsDir(versionHome: string): string;
189
+ /** Resolve source module files for an agents-cli plugin to install into OpenCode. */
190
+ export declare function resolveOpenCodePluginSources(pluginRoot: string): string[];
191
+ export declare function installOpenCodePlugin(plugin: DiscoveredPlugin, versionHome: string): boolean;
192
+ export declare function isOpenCodePluginInstalled(pluginName: string, versionHome: string): boolean;
193
+ export declare function removeOpenCodePlugin(pluginName: string, versionHome: string): boolean;
194
+ /**
195
+ * Goose auto-discovers Open Plugins at `$HOME/.agents/plugins/<name>/`.
196
+ * Under agents-cli version isolation HOME is the version home, so we install to:
197
+ * {versionHome}/.agents/plugins/<name>/
198
+ *
199
+ * Full plugin directory copy (not marketplace) — same layout goose and
200
+ * agents-cli share for source plugins.
201
+ */
202
+ export declare function goosePluginsDir(versionHome: string): string;
203
+ export declare function installGoosePlugin(plugin: DiscoveredPlugin, versionHome: string): boolean;
204
+ export declare function isGoosePluginInstalled(pluginName: string, versionHome: string): boolean;
205
+ export declare function removeGoosePlugin(pluginName: string, versionHome: string): boolean;
171
206
  /**
172
207
  * Check if a plugin is synced to a version. True when the plugin lives at the
173
208
  * native marketplace install path. Legacy dual-dash entries are not counted —