pactive 0.0.1-alpha.1 → 0.0.3-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- // DSH host adapter (contract §2). argv is verbatim per design §1: acti owns a dedicated profile.
1
+ // DSH host adapter (contract §2). argv is verbatim per design §1: pactive owns a dedicated profile.
2
2
  import { readFileSync, readdirSync, rmSync } from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
@@ -17,7 +17,7 @@ export const MODE = 'profile';
17
17
  export const resolveExec = makeResolver('dsh', 'dsh');
18
18
 
19
19
  export function buildArgv({ pkgRoot, userArgs = [] }) {
20
- return ['--profile', 'acti', ...userArgs];
20
+ return ['--profile', 'pactive', ...userArgs];
21
21
  }
22
22
 
23
23
  export function plan({ pkgRoot, env = process.env, userArgs = [] }) {
@@ -62,13 +62,13 @@ export function stateRoot(ctx = {}) {
62
62
  }
63
63
 
64
64
  export function installArgs({ pkgRoot }) {
65
- return ['plugin', '--profile', 'acti', 'add', pkgRoot];
65
+ return ['plugin', '--profile', 'pactive', 'add', pkgRoot];
66
66
  }
67
67
 
68
68
  export function uninstallArgs({ pkgRoot }) {
69
69
  // pnpm removes by DEPENDENCY NAME, not by path: `pnpm remove /abs/path` exits 1 and leaves
70
70
  // the profile untouched (defect found 2026-11-05 via install→uninstall→residue probe).
71
- return ['plugin', '--profile', 'acti', 'remove', packageName(pkgRoot)];
71
+ return ['plugin', '--profile', 'pactive', 'remove', packageName(pkgRoot)];
72
72
  }
73
73
 
74
74
  /** The dependency name that `install` registered (falls back to the package's own default). */
@@ -79,25 +79,25 @@ function packageName(pkgRoot) {
79
79
  } catch {
80
80
  // manifest unreadable: fall back to the contract name
81
81
  }
82
- return 'acti';
82
+ return 'pactive';
83
83
  }
84
84
 
85
- /** Whether the acti profile currently exists (used to keep `uninstall` idempotent and quiet). */
85
+ /** Whether the pactive profile currently exists (used to keep `uninstall` idempotent and quiet). */
86
86
  export function isInstalled({ env }) {
87
87
  const home = env.DSH_HOME || path.join(os.homedir(), '.dsh');
88
- const manifest = path.join(home, 'profiles', 'acti', 'package.json');
88
+ const manifest = path.join(home, 'profiles', 'pactive', 'package.json');
89
89
  try {
90
90
  const pkg = JSON.parse(readFileSync(manifest, 'utf8'));
91
- return Boolean(pkg?.dependencies?.acti) || (pkg?.dsh?.profile?.bundles || []).includes('acti');
91
+ return Boolean(pkg?.dependencies?.pactive) || (pkg?.dsh?.profile?.bundles || []).includes('pactive');
92
92
  } catch {
93
93
  return false;
94
94
  }
95
95
  }
96
96
 
97
- /** Remove the acti-owned DSH profile directory so `uninstall` leaves zero residue (I40). */
97
+ /** Remove the pactive-owned DSH profile directory so `uninstall` leaves zero residue (I40). */
98
98
  export function cleanup({ env }) {
99
99
  const home = env.DSH_HOME || path.join(os.homedir(), '.dsh');
100
- const profileDir = path.join(home, 'profiles', 'acti');
100
+ const profileDir = path.join(home, 'profiles', 'pactive');
101
101
  const manifest = path.join(profileDir, 'package.json');
102
102
  let pkg;
103
103
  try {
@@ -105,7 +105,7 @@ export function cleanup({ env }) {
105
105
  } catch {
106
106
  return { removed: false, reason: 'no-profile' };
107
107
  }
108
- if (pkg.name !== 'dsh-profile-acti') {
108
+ if (pkg.name !== 'dsh-profile-pactive') {
109
109
  return { removed: false, reason: 'profile-not-owned' };
110
110
  }
111
111
  rmSync(profileDir, { recursive: true, force: true });
@@ -120,7 +120,7 @@ export function cleanup({ env }) {
120
120
  }
121
121
 
122
122
  export function verifyArgs({ pkgRoot }) {
123
- return ['plugin', '--profile', 'acti', 'list'];
123
+ return ['plugin', '--profile', 'pactive', 'list'];
124
124
  }
125
125
 
126
126
  export function verifyMatch({ pkgRoot }, output) {
@@ -1,17 +1,17 @@
1
1
  // Contract §2 error taxonomy.
2
- export class ActiError extends Error {
3
- constructor(message, { code = 'ACTI_ERROR', exitCode = 1, cause } = {}) {
2
+ export class PactiveError extends Error {
3
+ constructor(message, { code = 'PACTIVE_ERROR', exitCode = 1, cause } = {}) {
4
4
  super(message, cause === undefined ? undefined : { cause });
5
- this.name = 'ActiError';
5
+ this.name = 'PactiveError';
6
6
  this.code = code;
7
7
  this.exitCode = exitCode;
8
8
  }
9
9
  }
10
10
 
11
11
  /** The host binary could not be found → CLI exit code 127. */
12
- export class HostMissingError extends ActiError {
12
+ export class HostMissingError extends PactiveError {
13
13
  constructor(host, attempted = null) {
14
- const envVar = `ACTI_${host.toUpperCase()}_BIN`;
14
+ const envVar = `PACTIVE_${host.toUpperCase()}_BIN`;
15
15
  super(
16
16
  `host binary for '${host}' not found${attempted ? ` (${attempted})` : ''} — install '${host}' or set ${envVar}`,
17
17
  { code: 'HOST_MISSING', exitCode: 127 },
@@ -19,7 +19,7 @@ export function buildArgv({ pkgRoot, userArgs = [] }) {
19
19
  '--extension',
20
20
  path.join(pkgRoot, 'pi', 'extension.ts'),
21
21
  '--skill',
22
- path.join(pkgRoot, 'pi', 'skills', 'acti'),
22
+ path.join(pkgRoot, 'pi', 'skills', 'pactive'),
23
23
  ...userArgs,
24
24
  ];
25
25
  }
@@ -26,10 +26,10 @@ export function findOnPath(name, env = process.env) {
26
26
  return null;
27
27
  }
28
28
 
29
- /** env.ACTI_<HOST>_BIN → PATH lookup → throw HostMissingError (exit 127). */
29
+ /** env.PACTIVE_<HOST>_BIN → PATH lookup → throw HostMissingError (exit 127). */
30
30
  export function makeResolver(host, binaryName) {
31
31
  return function resolveExec({ env = process.env } = {}) {
32
- const override = env[`ACTI_${host.toUpperCase()}_BIN`];
32
+ const override = env[`PACTIVE_${host.toUpperCase()}_BIN`];
33
33
  if (override) {
34
34
  if (isExecutable(override)) return path.resolve(override);
35
35
  throw new HostMissingError(host, override);
@@ -60,5 +60,5 @@ export function unreachableReceipt(reason) {
60
60
  }
61
61
 
62
62
  export function defaultStateRoot(ctx = {}) {
63
- return ctx.stateRoot ?? ctx.paths?.root ?? path.join(os.homedir(), '.acti');
63
+ return ctx.stateRoot ?? ctx.paths?.root ?? path.join(os.homedir(), '.pactive');
64
64
  }
package/bin/acti.mjs DELETED
@@ -1,439 +0,0 @@
1
- #!/usr/bin/env node
2
- // Acti CLI — contract §3 / design §6.
3
- //
4
- // Output discipline (design §6.3): machine-readable results (--print-plan, doctor --json,
5
- // --version) go to stdout; every human/diagnostic Acti text goes to stderr, so the stdout of a
6
- // spawned host is never polluted.
7
- import { spawn, spawnSync } from 'node:child_process';
8
- import { existsSync, readdirSync, readFileSync, realpathSync, rmSync, statSync } from 'node:fs';
9
- import os from 'node:os';
10
- import path from 'node:path';
11
- import { fileURLToPath, pathToFileURL } from 'node:url';
12
-
13
- import { ActiError } from '../src/hosts/errors.mjs';
14
-
15
- const HOSTS = ['pi', 'opcteam', 'dsh'];
16
- const CLI_DIR = path.dirname(fileURLToPath(import.meta.url));
17
- const PKG_ROOT = path.resolve(CLI_DIR, '..');
18
- const FALLBACK_VERSION = '0.0.1-alpha.1';
19
- const STALE_LOCK_MS = 30 * 60 * 1000;
20
-
21
- function usage(io) {
22
- io.write(`acti — proactive companion for Pi / OPCteam / DSH
23
-
24
- Usage:
25
- acti <host> [args...] run a host with acti injected (stdio inherited)
26
- acti --print-plan <host> print the launch plan as JSON (no side effects)
27
- acti doctor [--json] diagnose hosts, state root and permissions (read-only)
28
- acti install <host> register acti with a host (idempotent)
29
- acti uninstall <host> unregister acti and remove acti state residue
30
- acti --version | -v print the acti version
31
- acti --help | -h show this help
32
-
33
- Hosts: ${HOSTS.join(', ')}
34
-
35
- Environment:
36
- ACTI_HOME package root override (default: this package)
37
- ACTI_STATE_DIR acti state root override
38
- ACTI_PI_BIN override the pi executable
39
- ACTI_OPCTEAM_BIN override the opcteam executable
40
- ACTI_DSH_BIN override the dsh executable
41
- `);
42
- }
43
-
44
- function unknownHost(host, io) {
45
- io.write(`acti: unknown host '${host}'\n`);
46
- io.write(`acti: known hosts: ${HOSTS.join(', ')}\n`);
47
- }
48
-
49
- function resolvePkgRoot(env) {
50
- return env.ACTI_HOME ? path.resolve(env.ACTI_HOME) : PKG_ROOT;
51
- }
52
-
53
- function actiVersion() {
54
- try {
55
- const pkg = JSON.parse(readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8'));
56
- if (typeof pkg.version === 'string') return pkg.version;
57
- } catch {
58
- // package.json not built yet — fall back to the contract version
59
- }
60
- return FALLBACK_VERSION;
61
- }
62
-
63
- async function loadAdapter(host) {
64
- return import(new URL(`../src/hosts/${host}/index.mjs`, import.meta.url));
65
- }
66
-
67
- /** src/core is built by the acti-core worker; until then the CLI degrades instead of faking it. */
68
- async function loadCore() {
69
- try {
70
- return await import(new URL('../src/core/index.mjs', import.meta.url));
71
- } catch (err) {
72
- throw new ActiError('ACTI core not built yet', { code: 'CORE_MISSING', exitCode: 1, cause: err });
73
- }
74
- }
75
-
76
- async function resolveStateRoot(env, warnings = []) {
77
- if (env.ACTI_STATE_DIR) return path.resolve(env.ACTI_STATE_DIR);
78
- try {
79
- const core = await loadCore();
80
- const paths = core.resolvePaths('pi', env);
81
- if (paths?.root) return paths.root;
82
- warnings.push('ACTI core returned no state root — using the default location');
83
- } catch (err) {
84
- warnings.push(`${err.message} — using the default state root`);
85
- }
86
- return path.join(os.homedir(), '.acti');
87
- }
88
-
89
- function modeOf(p) {
90
- return (statSync(p).mode & 0o777).toString(8).padStart(4, '0');
91
- }
92
-
93
- function probeVersion(exec) {
94
- const res = spawnSync(exec, ['--version'], { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'pipe'] });
95
- const out = `${res.stdout ?? ''}${res.stderr ?? ''}`.trim();
96
- if (!out) return null;
97
- return out.split(/\s+/)[0];
98
- }
99
-
100
- function majorOf(version) {
101
- const major = Number.parseInt(String(version).split('.')[0], 10);
102
- return Number.isNaN(major) ? null : major;
103
- }
104
-
105
- function isCompatible(found, supported) {
106
- if (!found || !supported) return false;
107
- const a = majorOf(found);
108
- const b = majorOf(supported);
109
- return a !== null && b !== null && a === b;
110
- }
111
-
112
- function countFiles(dir) {
113
- if (!existsSync(dir)) return 0;
114
- let n = 0;
115
- for (const name of readdirSync(dir)) {
116
- const abs = path.join(dir, name);
117
- if (statSync(abs).isDirectory()) n += countFiles(abs);
118
- else n += 1;
119
- }
120
- return n;
121
- }
122
-
123
- function stateStats(stateRoot) {
124
- const stats = {
125
- exists: existsSync(stateRoot),
126
- perm: { dir: null, file: null },
127
- idle: { enabled: false },
128
- sessions: 0,
129
- tasks: 0,
130
- outbox: 0,
131
- locksStale: 0,
132
- fails: [],
133
- };
134
- if (!stats.exists) return stats;
135
-
136
- stats.perm.dir = modeOf(stateRoot);
137
- if (stats.perm.dir !== '0700') {
138
- stats.fails.push(`state root ${stateRoot} has mode ${stats.perm.dir}, expected 0700`);
139
- }
140
-
141
- const stateFile = path.join(stateRoot, 'state.json');
142
- if (existsSync(stateFile)) {
143
- stats.perm.file = modeOf(stateFile);
144
- if (stats.perm.file !== '0600') {
145
- stats.fails.push(`state file ${stateFile} has mode ${stats.perm.file}, expected 0600`);
146
- }
147
- try {
148
- const state = JSON.parse(readFileSync(stateFile, 'utf8'));
149
- stats.idle.enabled = Boolean(state?.idle?.enabled);
150
- if (Array.isArray(state?.tasks)) stats.tasks = state.tasks.length;
151
- } catch {
152
- stats.fails.push(`state file ${stateFile} is not valid JSON`);
153
- }
154
- }
155
-
156
- const tasksFile = path.join(stateRoot, 'tasks.json');
157
- if (existsSync(tasksFile)) {
158
- try {
159
- const tasks = JSON.parse(readFileSync(tasksFile, 'utf8'));
160
- if (Array.isArray(tasks)) stats.tasks = tasks.length;
161
- } catch {
162
- stats.fails.push(`tasks file ${tasksFile} is not valid JSON`);
163
- }
164
- }
165
-
166
- stats.sessions = countFiles(path.join(stateRoot, 'peers'));
167
- stats.outbox = countFiles(path.join(stateRoot, 'outbox'));
168
-
169
- const locksDir = path.join(stateRoot, 'locks');
170
- if (existsSync(locksDir)) {
171
- const now = Date.now();
172
- for (const name of readdirSync(locksDir)) {
173
- const abs = path.join(locksDir, name);
174
- if (statSync(abs).isFile() && now - statSync(abs).mtimeMs > STALE_LOCK_MS) stats.locksStale += 1;
175
- }
176
- }
177
- return stats;
178
- }
179
-
180
- async function doctorReport(env) {
181
- const warnings = [];
182
- const stateRoot = await resolveStateRoot(env, warnings);
183
- const hosts = [];
184
- for (const host of HOSTS) {
185
- const mod = await loadAdapter(host);
186
- let present = false;
187
- let version = null;
188
- let compatible = false;
189
- try {
190
- const exec = mod.resolveExec({ env });
191
- present = true;
192
- version = probeVersion(exec);
193
- compatible = isCompatible(version, mod.SUPPORTED_VERSION);
194
- if (!version) {
195
- warnings.push(`host ${host}: installed but its version could not be read`);
196
- } else if (!compatible) {
197
- warnings.push(`host ${host}: version ${version} differs from the supported ${mod.SUPPORTED_VERSION}`);
198
- }
199
- } catch (err) {
200
- if (err.code !== 'HOST_MISSING') throw err;
201
- warnings.push(`host ${host}: not installed (set ACTI_${host.toUpperCase()}_BIN to override)`);
202
- }
203
- hosts.push({ host, present, version, mode: mod.MODE, compatible });
204
- }
205
-
206
- const stats = stateStats(stateRoot);
207
- if (!stats.exists) warnings.push(`state root ${stateRoot} does not exist yet`);
208
- for (const fail of stats.fails) warnings.push(`FAIL: ${fail}`);
209
-
210
- return {
211
- ok: stats.fails.length === 0,
212
- hosts,
213
- stateRoot,
214
- perm: stats.perm,
215
- idle: { enabled: stats.idle.enabled, source: 'core-timeline' },
216
- sessions: stats.sessions,
217
- tasks: stats.tasks,
218
- outbox: stats.outbox,
219
- locksStale: stats.locksStale,
220
- secrets: { credentialsRead: false },
221
- warnings,
222
- };
223
- }
224
-
225
- function printPlan(args, env, io) {
226
- const host = args[0];
227
- if (!host) {
228
- unknownHost('', io.stderr);
229
- return Promise.resolve(2);
230
- }
231
- if (!HOSTS.includes(host)) {
232
- unknownHost(host, io.stderr);
233
- return Promise.resolve(2);
234
- }
235
- return loadAdapter(host).then((mod) => {
236
- const plan = mod.plan({ pkgRoot: resolvePkgRoot(env), env, userArgs: [] });
237
- io.stdout.write(`${JSON.stringify(plan, null, 2)}\n`);
238
- return 0;
239
- });
240
- }
241
-
242
- function doctorCmd(args, env, io) {
243
- const asJson = args.includes('--json');
244
- return doctorReport(env).then((report) => {
245
- if (asJson) {
246
- io.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
247
- } else {
248
- io.stderr.write(`acti doctor: ${report.ok ? 'OK' : 'FAIL'}\n`);
249
- io.stderr.write(`state root: ${report.stateRoot} (dir ${report.perm.dir}, file ${report.perm.file})\n`);
250
- for (const h of report.hosts) {
251
- io.stderr.write(
252
- `host ${h.host}: present=${h.present} version=${h.version ?? 'unknown'} mode=${h.mode} compatible=${h.compatible}\n`,
253
- );
254
- }
255
- io.stderr.write(
256
- `sessions=${report.sessions} tasks=${report.tasks} outbox=${report.outbox} locksStale=${report.locksStale} idle.enabled=${report.idle.enabled}\n`,
257
- );
258
- for (const w of report.warnings) io.stderr.write(`- ${w}\n`);
259
- }
260
- return report.ok ? 0 : 1;
261
- });
262
- }
263
-
264
- function runHostCommand(exec, args) {
265
- const res = spawnSync(exec, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
266
- return { status: res.status ?? 1, stdout: res.stdout ?? '', stderr: res.stderr ?? '' };
267
- }
268
-
269
- function verifyRegistered(mod, exec, pkgRoot) {
270
- const res = runHostCommand(exec, mod.verifyArgs({ pkgRoot }));
271
- return mod.verifyMatch({ pkgRoot }, `${res.stdout}\n${res.stderr}`);
272
- }
273
-
274
- const OWNED_STATE_ENTRIES = ['state.json', 'tasks.json', 'ledger.jsonl', 'peers', 'inbox', 'outbox', 'locks'];
275
-
276
- function removeStateResidue(stateRoot) {
277
- if (!existsSync(stateRoot)) return [];
278
- for (const name of OWNED_STATE_ENTRIES) {
279
- rmSync(path.join(stateRoot, name), { recursive: true, force: true });
280
- }
281
- for (const name of readdirSync(stateRoot)) {
282
- if (/^(state|tasks)\.json\.corrupt-/.test(name)) {
283
- rmSync(path.join(stateRoot, name), { recursive: true, force: true });
284
- }
285
- }
286
- const left = [];
287
- const walk = (dir, rel) => {
288
- for (const name of readdirSync(dir)) {
289
- const abs = path.join(dir, name);
290
- const relPath = rel ? path.join(rel, name) : name;
291
- if (statSync(abs).isDirectory()) walk(abs, relPath);
292
- else left.push(relPath);
293
- }
294
- };
295
- walk(stateRoot, '');
296
- return left;
297
- }
298
-
299
- function installCmd(args, env, io, action) {
300
- const host = args[0];
301
- if (!host) {
302
- unknownHost('', io.stderr);
303
- return Promise.resolve(2);
304
- }
305
- if (!HOSTS.includes(host)) {
306
- unknownHost(host, io.stderr);
307
- return Promise.resolve(2);
308
- }
309
- return loadAdapter(host).then(async (mod) => {
310
- const exec = mod.resolveExec({ env });
311
- const pkgRoot = resolvePkgRoot(env);
312
- if (action === 'uninstall' && typeof mod.isInstalled === 'function') {
313
- let installed = true;
314
- try { installed = mod.isInstalled({ pkgRoot, env }); } catch { installed = true; }
315
- if (!installed) {
316
- // 幂等:没装就什么都不跑(跑宿主 remove 会让 DSH 重建 profile,制造"删了又出现"的怪象)
317
- if (typeof mod.cleanup === 'function') {
318
- try { mod.cleanup({ pkgRoot, env }); } catch { /* residue 清理失败由下方 residue 检查兜底 */ }
319
- }
320
- io.stderr.write(`acti: ${host} not installed — nothing to uninstall\n`);
321
- return 0;
322
- }
323
- }
324
- const argsFor = action === 'install' ? mod.installArgs({ pkgRoot }) : mod.uninstallArgs({ pkgRoot });
325
- const res = runHostCommand(exec, argsFor);
326
-
327
- if (res.status !== 0) {
328
- const stillRegistered = verifyRegistered(mod, exec, pkgRoot);
329
- if (action === 'install' ? !stillRegistered : stillRegistered) {
330
- io.stderr.write(`acti: ${action} ${host} failed (exit ${res.status})\n`);
331
- if (res.stderr.trim()) io.stderr.write(`${res.stderr.trim()}\n`);
332
- return 1;
333
- }
334
- }
335
-
336
- if (action === 'uninstall') {
337
- // Host-level residue: adapters may own resources Acti created (e.g. the DSH profile dir).
338
- if (typeof mod.cleanup === 'function') {
339
- try {
340
- const cleaned = mod.cleanup({ pkgRoot, env });
341
- if (cleaned && cleaned.removed) io.stderr.write(`acti: removed ${cleaned.path}\n`);
342
- } catch (err) {
343
- io.stderr.write(`acti: cleanup warning: ${err?.message ?? err}\n`);
344
- }
345
- }
346
- const stateRoot = await resolveStateRoot(env, []);
347
- const residue = removeStateResidue(stateRoot);
348
- if (residue.length > 0) {
349
- io.stderr.write(`acti: residue left in ${stateRoot}: ${residue.join(', ')}\n`);
350
- return 1;
351
- }
352
- }
353
-
354
- io.stderr.write(`acti: ${action} ${host} ok (${pkgRoot})\n`);
355
- return 0;
356
- });
357
- }
358
-
359
- function runHost(host, userArgs, env, io) {
360
- return loadAdapter(host).then((mod) => {
361
- const exec = mod.resolveExec({ env });
362
- const pkgRoot = resolvePkgRoot(env);
363
- const argv = mod.buildArgv({ pkgRoot, userArgs });
364
- io.stderr.write(`acti: launching ${host} (${exec})\n`);
365
- return new Promise((resolve) => {
366
- const child = spawn(exec, argv, { stdio: 'inherit', env });
367
- const forward = (sig) => {
368
- if (!child.killed) child.kill(sig);
369
- };
370
- process.on('SIGINT', forward);
371
- process.on('SIGTERM', forward);
372
- child.on('error', (err) => {
373
- io.stderr.write(`acti: failed to launch ${host}: ${err.message}\n`);
374
- resolve(err.code === 'ENOENT' ? 127 : 1);
375
- });
376
- child.on('exit', (code, signal) => {
377
- process.off('SIGINT', forward);
378
- process.off('SIGTERM', forward);
379
- resolve(signal ? 128 + (os.constants.signals[signal] ?? 0) : (code ?? 0));
380
- });
381
- });
382
- });
383
- }
384
-
385
- export async function main(argv, env = process.env, io = { stdout: process.stdout, stderr: process.stderr }) {
386
- const args = argv.slice(2);
387
- if (args.length === 0) {
388
- usage(io.stderr);
389
- return 2;
390
- }
391
- const [cmd, ...rest] = args;
392
- switch (cmd) {
393
- case '--version':
394
- case '-v':
395
- io.stdout.write(`${actiVersion()}\n`);
396
- return 0;
397
- case '--help':
398
- case '-h':
399
- usage(io.stderr);
400
- return 0;
401
- case '--print-plan':
402
- return printPlan(rest, env, io);
403
- case 'doctor':
404
- return doctorCmd(rest, env, io);
405
- case 'install':
406
- return installCmd(rest, env, io, 'install');
407
- case 'uninstall':
408
- return installCmd(rest, env, io, 'uninstall');
409
- default:
410
- break;
411
- }
412
- if (!HOSTS.includes(cmd)) {
413
- unknownHost(cmd, io.stderr);
414
- return 2;
415
- }
416
- return runHost(cmd, rest, env, io);
417
- }
418
-
419
- // Entry guard: compare REAL paths. Installed bin shims and macOS /tmp are symlinks, so
420
- // `process.argv[1]` and `import.meta.url` differ textually when they point at the same file —
421
- // a plain string compare silently turns the CLI into a no-op (defect found 2026-11-05 via install chain).
422
- const isMain = (() => {
423
- try {
424
- const self = realpathSync(fileURLToPath(import.meta.url));
425
- const argv1 = realpathSync(process.argv[1]);
426
- return self === argv1;
427
- } catch {
428
- return false;
429
- }
430
- })();
431
- if (isMain) {
432
- try {
433
- process.exitCode = await main(process.argv, process.env, { stdout: process.stdout, stderr: process.stderr });
434
- } catch (err) {
435
- const exitCode = err instanceof ActiError ? err.exitCode : 1;
436
- process.stderr.write(`acti: ${err?.message ?? err}\n`);
437
- process.exitCode = exitCode;
438
- }
439
- }
package/commands/acti.md DELETED
@@ -1,46 +0,0 @@
1
- ---
2
- description: Acti 主动机制 — status、schedule、tell、history(经 acti MCP server)
3
- argument-hint: "[status|schedule|tell|history] [args...]"
4
- ---
5
-
6
- # acti:acti
7
-
8
- 把用户关于「主动机制 / 空闲触发 / 定时任务 / 跨会话消息」的请求路由到 `acti` MCP server。
9
- 宿主把它暴露为 `/acti:acti`;裸 `/acti` 不由插件契约保证。
10
-
11
- ## 工具名
12
-
13
- OPCteam 用命名空间暴露插件 MCP 工具:
14
-
15
- ```
16
- mcp__plugin_acti_acti__<tool>
17
- ```
18
-
19
- 具体名字:
20
-
21
- - `mcp__plugin_acti_acti__acti_status`
22
- - `mcp__plugin_acti_acti__acti_schedule`
23
- - `mcp__plugin_acti_acti__acti_tell`
24
- - `mcp__plugin_acti_acti__acti_history`
25
-
26
- 裸名(`acti_status`)不一定能被 ToolSearch 发现;若命名空间工具没加载,用
27
- `select:mcp__plugin_acti_acti__acti_status` 搜一次。
28
-
29
- ## 路由
30
-
31
- | 用户说 | 调用 |
32
- |---|---|
33
- | 状态 / 为什么没触发 | `acti_status`(看 `idle.reason`,别猜) |
34
- | 10 分钟后提醒我 / 定时 | `acti_schedule` `when:"10m"` `prompt:"…"` |
35
- | 18:30 提醒我 | `acti_schedule` `when:"18:30"` |
36
- | 每小时 | `acti_schedule` `when:"every 1h"` |
37
- | 告诉另一个会话 | `acti_tell` `to` `body`(可选 `kind:"ask"`) |
38
- | 最近发生过什么 | `acti_history` `limit` |
39
-
40
- ## 硬规则
41
-
42
- 1. **不要替用户改配置**:acti 的开关与配置是人工闸门(CLI `--confirm`)。用户要求改配置时,
43
- 告诉他用 `acti config set … --confirm`,不要自行绕过。
44
- 2. **回执如实转述**:`queued` 就是**还没送到**(对端离线),不要说成已送达。
45
- 3. **默认不打扰**:空闲触发默认关闭;用户没明确要「空闲自动推进」时,不要假设它已开启。
46
- 4. **只报新事实**:状态类输出要给出 `idle.reason` 与护栏结论,不要泛泛说「一切正常」。
@@ -1,47 +0,0 @@
1
- ---
2
- name: acti
3
- description: >-
4
- Correct usage of the acti proactive-assistant extension (tools acti_status / acti_schedule /
5
- acti_tell / acti_history and the "/acti" command family). Use when the task involves checking
6
- whether the idle trigger fired or why it did not, scheduling work for later (one-shot or
7
- repeating), reading the acti event ledger, or sending a message to another session on the same
8
- machine. Covers the semantics traps (queued is not delivered, idle is disabled by default,
9
- human-gated config needs CLI --confirm, the Stop hook blocks at most once per turn) and the
10
- "when" syntax (10m / 18:30 / ISO 8601 / every 1h).
11
- ---
12
-
13
- # acti:怎么正确用它
14
-
15
- acti 给会话加**主动机制**:空闲判定(到点才动手)、**定时任务**、**跨会话消息**与**事件账本**。
16
-
17
- ## 工具与命令
18
-
19
- | 入口 | 作用 |
20
- |---|---|
21
- | `acti_status` / `/acti status` | 空闲判定与 `reason`、护栏四闸门、定时任务数、最近活动(只读) |
22
- | `acti_schedule` / `/acti schedule <when> <prompt>` | 登记一个定时任务(真正落盘,重启后仍在) |
23
- | `acti_tell <to> <body>` | 给同机另一个会话投递消息(离线时排队,不伪装成功) |
24
- | `acti_history [n]` | 读事件账本尾部:触发 → 投递 → 回应 → 结账 |
25
- | `/acti tasks` | 列出待触发的定时任务 |
26
-
27
- ## 关键语义(别踩)
28
-
29
- - **默认不打扰**:`idle.enabled` 默认 `false`。没打开开关,空闲永远不会触发主动行为。
30
- - **`reason` 是枚举**:`idle | disabled | no-control | busy | threshold | quiet | hourly-limit | min-interval | cooldown | requires-action`。
31
- 没触发时先看 `reason`,不要猜。
32
- - **四闸门缺一不可**:控制权已交回 + 不忙 + 超过阈值 + 护栏通过,才可能触发。
33
- - **`queued` ≠ 已送达**:对端离线时消息进 outbox,等对端上线再补投;回执写 `queued` 就是还没送到。
34
- - **配置是人工闸门**:`config` 只能由人用 CLI `--confirm` 改;模型/MCP 侧改配置一律得到
35
- `need_human_confirmation`,且配置不变。
36
- - **Stop hook 最多阻断一次**:同一轮里只阻断一次,避免死循环;`UserPromptSubmit` 会重置这个计数。
37
-
38
- ## `when` 语法
39
-
40
- `10m` / `2h` / `30s`(相对时长)、`18:30`(当天时刻,已过则顺延到明天)、
41
- ISO 8601 时间戳(绝对时刻)、`every 1h`(周期)。缺省 `10m`。
42
-
43
- ## 排障
44
-
45
- - 状态面板里 `reason=disabled` → 开关没开;`reason=busy` → 宿主还在忙;`reason=threshold` → 还没到阈值。
46
- - 任务登记了但没触发 → 先看 `acti_history` 里有没有 `triggered` 记录;没有说明调度器没跑到。
47
- - 消息一直是 `queued` → 对端不在线;看 `acti_status` 的 peers。