@moxxy/plugin-self-update 0.26.0

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 (89) hide show
  1. package/LICENSE +21 -0
  2. package/dist/classify.d.ts +33 -0
  3. package/dist/classify.d.ts.map +1 -0
  4. package/dist/classify.js +177 -0
  5. package/dist/classify.js.map +1 -0
  6. package/dist/core-tools/apply.d.ts +4 -0
  7. package/dist/core-tools/apply.d.ts.map +1 -0
  8. package/dist/core-tools/apply.js +43 -0
  9. package/dist/core-tools/apply.js.map +1 -0
  10. package/dist/core-tools/begin.d.ts +4 -0
  11. package/dist/core-tools/begin.d.ts.map +1 -0
  12. package/dist/core-tools/begin.js +76 -0
  13. package/dist/core-tools/begin.js.map +1 -0
  14. package/dist/core-tools/edit.d.ts +4 -0
  15. package/dist/core-tools/edit.d.ts.map +1 -0
  16. package/dist/core-tools/edit.js +33 -0
  17. package/dist/core-tools/edit.js.map +1 -0
  18. package/dist/core-tools/index.d.ts +11 -0
  19. package/dist/core-tools/index.d.ts.map +1 -0
  20. package/dist/core-tools/index.js +28 -0
  21. package/dist/core-tools/index.js.map +1 -0
  22. package/dist/core-tools/preflight.d.ts +4 -0
  23. package/dist/core-tools/preflight.d.ts.map +1 -0
  24. package/dist/core-tools/preflight.js +14 -0
  25. package/dist/core-tools/preflight.js.map +1 -0
  26. package/dist/core-tools/rollback.d.ts +4 -0
  27. package/dist/core-tools/rollback.d.ts.map +1 -0
  28. package/dist/core-tools/rollback.js +30 -0
  29. package/dist/core-tools/rollback.js.map +1 -0
  30. package/dist/core-tools/shared.d.ts +16 -0
  31. package/dist/core-tools/shared.d.ts.map +1 -0
  32. package/dist/core-tools/shared.js +12 -0
  33. package/dist/core-tools/shared.js.map +1 -0
  34. package/dist/core-tools/status.d.ts +4 -0
  35. package/dist/core-tools/status.d.ts.map +1 -0
  36. package/dist/core-tools/status.js +25 -0
  37. package/dist/core-tools/status.js.map +1 -0
  38. package/dist/core-tools/verify.d.ts +4 -0
  39. package/dist/core-tools/verify.d.ts.map +1 -0
  40. package/dist/core-tools/verify.js +39 -0
  41. package/dist/core-tools/verify.js.map +1 -0
  42. package/dist/core-tools/write.d.ts +4 -0
  43. package/dist/core-tools/write.d.ts.map +1 -0
  44. package/dist/core-tools/write.js +28 -0
  45. package/dist/core-tools/write.js.map +1 -0
  46. package/dist/core-update.d.ts +173 -0
  47. package/dist/core-update.d.ts.map +1 -0
  48. package/dist/core-update.js +626 -0
  49. package/dist/core-update.js.map +1 -0
  50. package/dist/deps.d.ts +49 -0
  51. package/dist/deps.d.ts.map +1 -0
  52. package/dist/deps.js +30 -0
  53. package/dist/deps.js.map +1 -0
  54. package/dist/index.d.ts +22 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +349 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/transaction.d.ts +80 -0
  59. package/dist/transaction.d.ts.map +1 -0
  60. package/dist/transaction.js +188 -0
  61. package/dist/transaction.js.map +1 -0
  62. package/dist/verify.d.ts +23 -0
  63. package/dist/verify.d.ts.map +1 -0
  64. package/dist/verify.js +164 -0
  65. package/dist/verify.js.map +1 -0
  66. package/package.json +61 -0
  67. package/src/classify.test.ts +176 -0
  68. package/src/classify.ts +221 -0
  69. package/src/core-tools/apply.ts +44 -0
  70. package/src/core-tools/begin.ts +94 -0
  71. package/src/core-tools/edit.ts +33 -0
  72. package/src/core-tools/index.ts +33 -0
  73. package/src/core-tools/preflight.ts +15 -0
  74. package/src/core-tools/rollback.ts +30 -0
  75. package/src/core-tools/shared.test.ts +16 -0
  76. package/src/core-tools/shared.ts +24 -0
  77. package/src/core-tools/status.ts +26 -0
  78. package/src/core-tools/verify.ts +39 -0
  79. package/src/core-tools/write.ts +30 -0
  80. package/src/core-update.test.ts +542 -0
  81. package/src/core-update.ts +744 -0
  82. package/src/deps.ts +84 -0
  83. package/src/discovery.test.ts +40 -0
  84. package/src/index.test.ts +299 -0
  85. package/src/index.ts +441 -0
  86. package/src/transaction.test.ts +143 -0
  87. package/src/transaction.ts +254 -0
  88. package/src/verify.test.ts +82 -0
  89. package/src/verify.ts +200 -0
@@ -0,0 +1,188 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { promises as fs } from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import { writeFileAtomic } from '@moxxy/sdk/server';
5
+ /** Names present in `after` but not in `before`, per kind. Empty keys dropped. */
6
+ export function diffSnapshot(before, after) {
7
+ const out = {};
8
+ for (const key of Object.keys(after)) {
9
+ const had = new Set(before[key] ?? []);
10
+ const added = (after[key] ?? []).filter((n) => !had.has(n));
11
+ if (added.length > 0)
12
+ out[key] = added;
13
+ }
14
+ return out;
15
+ }
16
+ /** Max failed verify cycles for one transaction before we force escalation. */
17
+ export const MAX_FAILED_ATTEMPTS = 2;
18
+ const NAME_SEGMENT_RE = /^[A-Za-z0-9._-]+$/;
19
+ /**
20
+ * Paths excluded from a transaction snapshot. `node_modules` is reproducible
21
+ * from package.json (verify re-runs `npm install`), `.git` would balloon the
22
+ * copy, and `dist` is regenerated by the build — copying any of them turns a
23
+ * source snapshot into a multi-hundred-MB recursive copy of the dependency tree
24
+ * on every begin and every restore, which is slow and can exhaust disk.
25
+ */
26
+ const SNAPSHOT_EXCLUDE = new Set(['node_modules', '.git', 'dist']);
27
+ /**
28
+ * Build an `fs.cp` filter that skips excluded directory basenames so snapshots
29
+ * stay source-only — but never excludes the copy root itself (a plugin could,
30
+ * per NAME_SEGMENT_RE, legitimately be named e.g. `dist`).
31
+ */
32
+ function snapshotFilter(root) {
33
+ return (src) => src === root || !SNAPSHOT_EXCLUDE.has(path.basename(src));
34
+ }
35
+ export function selfUpdateRoot(moxxyDir) {
36
+ return path.join(moxxyDir, 'self-update', 'txns');
37
+ }
38
+ export function txnDir(moxxyDir, txnId) {
39
+ return path.join(selfUpdateRoot(moxxyDir), txnId);
40
+ }
41
+ /**
42
+ * Resolve the on-disk target for a (kind, name). Throws on unsafe names so a
43
+ * model-supplied name can never escape the user artifact directories.
44
+ */
45
+ export function resolveTarget(moxxyDir, kind, name) {
46
+ if (!NAME_SEGMENT_RE.test(name)) {
47
+ throw new Error(`invalid ${kind} name "${name}": only letters, digits, dot, dash and underscore are allowed`);
48
+ }
49
+ const p = kind === 'plugin'
50
+ ? path.join(moxxyDir, 'plugins', name)
51
+ : path.join(moxxyDir, 'skills', `${name}.md`);
52
+ return { kind, name, path: p };
53
+ }
54
+ export function newTxnId(now = new Date()) {
55
+ const stamp = now.toISOString().replace(/[:.]/g, '-');
56
+ // crypto UUID (not Math.random): two begins in the same second must never
57
+ // collide on the txn dir, which beginTransaction's recursive mkdir would
58
+ // silently share — clobbering an existing txn's journal + before snapshot.
59
+ const rand = randomUUID().slice(0, 8);
60
+ return `${stamp}-${rand}`;
61
+ }
62
+ async function pathExists(p) {
63
+ try {
64
+ await fs.access(p);
65
+ return true;
66
+ }
67
+ catch {
68
+ return false;
69
+ }
70
+ }
71
+ /**
72
+ * Open a transaction: snapshot the current target (if any) into the txn dir
73
+ * and write the initial journal. Safe to call for a not-yet-existing target.
74
+ */
75
+ export async function beginTransaction(opts) {
76
+ const now = opts.now ?? new Date();
77
+ const target = resolveTarget(opts.moxxyDir, opts.kind, opts.name);
78
+ const txnId = newTxnId(now);
79
+ const dir = txnDir(opts.moxxyDir, txnId);
80
+ await fs.mkdir(dir, { recursive: true });
81
+ const existedBefore = await pathExists(target.path);
82
+ if (existedBefore) {
83
+ const before = path.join(dir, 'before');
84
+ await fs.cp(target.path, before, { recursive: true, filter: snapshotFilter(target.path) });
85
+ }
86
+ const journal = {
87
+ txnId,
88
+ createdAt: now.toISOString(),
89
+ updatedAt: now.toISOString(),
90
+ target,
91
+ existedBefore,
92
+ state: 'open',
93
+ attempts: [],
94
+ };
95
+ await writeJournal(opts.moxxyDir, journal);
96
+ return journal;
97
+ }
98
+ export async function writeJournal(moxxyDir, journal) {
99
+ journal.updatedAt = new Date().toISOString();
100
+ const file = path.join(txnDir(moxxyDir, journal.txnId), 'journal.json');
101
+ await writeFileAtomic(file, JSON.stringify(journal, null, 2) + '\n');
102
+ }
103
+ export async function readJournal(moxxyDir, txnId) {
104
+ const file = path.join(txnDir(moxxyDir, txnId), 'journal.json');
105
+ const raw = await fs.readFile(file, 'utf8');
106
+ return JSON.parse(raw);
107
+ }
108
+ export async function listTransactions(moxxyDir) {
109
+ const root = selfUpdateRoot(moxxyDir);
110
+ let ids;
111
+ try {
112
+ ids = (await fs.readdir(root, { withFileTypes: true }))
113
+ .filter((e) => e.isDirectory())
114
+ .map((e) => e.name);
115
+ }
116
+ catch {
117
+ return [];
118
+ }
119
+ const out = [];
120
+ for (const id of ids) {
121
+ try {
122
+ out.push(await readJournal(moxxyDir, id));
123
+ }
124
+ catch {
125
+ // ignore half-written / corrupt txn dirs
126
+ }
127
+ }
128
+ return out.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
129
+ }
130
+ export function recordAttempt(journal, attempt) {
131
+ journal.attempts.push({ at: new Date().toISOString(), ...attempt });
132
+ }
133
+ export function failedAttemptCount(journal) {
134
+ return journal.attempts.filter((a) => !a.ok).length;
135
+ }
136
+ /**
137
+ * Restore the target to its pre-transaction state: copy the snapshot back, or
138
+ * delete the artifact if it was newly created. Idempotent.
139
+ *
140
+ * The snapshot is source-only (SNAPSHOT_EXCLUDE drops node_modules/.git/dist),
141
+ * so a blanket `rm -rf` of the target before copying would wipe the installed
142
+ * runtime deps — and nothing re-installs them, leaving a plugin that fails to
143
+ * load on the very next deps.reload(). To keep the working `node_modules` (and
144
+ * the other reproducible-but-uncaptured dirs) across a MODIFICATION rollback, we
145
+ * remove only the captured source entries and overlay the snapshot back on top,
146
+ * never touching the excluded dirs. A newly-created target (no snapshot) is still
147
+ * deleted wholesale.
148
+ */
149
+ export async function restoreSnapshot(moxxyDir, journal) {
150
+ const { target, existedBefore } = journal;
151
+ if (!existedBefore) {
152
+ await fs.rm(target.path, { recursive: true, force: true });
153
+ return;
154
+ }
155
+ const before = path.join(txnDir(moxxyDir, journal.txnId), 'before');
156
+ // A skill target is a single file, not a directory — replace it outright.
157
+ if (target.kind !== 'plugin') {
158
+ await fs.rm(target.path, { recursive: true, force: true });
159
+ await fs.cp(before, target.path, { recursive: true, filter: snapshotFilter(before) });
160
+ return;
161
+ }
162
+ // Plugin directory: clear only the non-excluded entries (the source the
163
+ // snapshot captured) so the live node_modules/.git/dist survive intact, then
164
+ // copy the source snapshot back over them.
165
+ let entries = [];
166
+ try {
167
+ entries = (await fs.readdir(target.path, { withFileTypes: true })).map((e) => e.name);
168
+ }
169
+ catch {
170
+ // Target dir vanished — recreate it from the snapshot below.
171
+ }
172
+ for (const name of entries) {
173
+ if (SNAPSHOT_EXCLUDE.has(name))
174
+ continue;
175
+ await fs.rm(path.join(target.path, name), { recursive: true, force: true });
176
+ }
177
+ await fs.mkdir(target.path, { recursive: true });
178
+ await fs.cp(before, target.path, { recursive: true, filter: snapshotFilter(before) });
179
+ }
180
+ /** Keep the most recent `keep` terminal transactions; delete older ones. */
181
+ export async function gcTransactions(moxxyDir, keep) {
182
+ const all = await listTransactions(moxxyDir);
183
+ const terminal = all.filter((j) => j.state === 'committed' || j.state === 'rolled_back' || j.state === 'escalated');
184
+ for (const j of terminal.slice(keep)) {
185
+ await fs.rm(txnDir(moxxyDir, j.txnId), { recursive: true, force: true });
186
+ }
187
+ }
188
+ //# sourceMappingURL=transaction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transaction.js","sourceRoot":"","sources":["../src/transaction.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import type { TxnTarget } from './transaction.js';
2
+ /**
3
+ * Build/test verification for a transaction target, run as child processes so a
4
+ * failing build can never crash the host. Pure I/O — no moxxy core imports.
5
+ */
6
+ export interface StageResult {
7
+ readonly stage: string;
8
+ readonly ok: boolean;
9
+ readonly message: string;
10
+ }
11
+ export interface RunCmdResult {
12
+ readonly exitCode: number;
13
+ readonly output: string;
14
+ }
15
+ /**
16
+ * Run a plugin's own `build` and `test` npm scripts when present. A plugin
17
+ * scaffolded as a zero-build `.mjs` (no scripts) passes straight through —
18
+ * jiti / dynamic-import handles it at reload time, which is the real load test.
19
+ */
20
+ export declare function verifyPluginBuild(target: TxnTarget): Promise<ReadonlyArray<StageResult>>;
21
+ /** Minimal structural check that a skill file has the required frontmatter. */
22
+ export declare function verifySkillFile(target: TxnTarget): Promise<StageResult>;
23
+ //# sourceMappingURL=verify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAElD;;;GAGG;AAEH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAgGD;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,CA4C9F;AAUD,+EAA+E;AAC/E,wBAAsB,eAAe,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAwB7E"}
package/dist/verify.js ADDED
@@ -0,0 +1,164 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { promises as fs } from 'node:fs';
3
+ import * as path from 'node:path';
4
+ const CMD_TIMEOUT_MS = 5 * 60_000;
5
+ /**
6
+ * Cap on retained child output. A model-triggered `npm run build/test` can emit
7
+ * unbounded output; holding it all in the live process risks OOM. Callers only
8
+ * read the trailing ~1.2KB, so a sliding tail is lossless for them.
9
+ */
10
+ const MAX_CMD_OUTPUT_BYTES = 512 * 1024;
11
+ function runCmd(cmd, args, cwd) {
12
+ return new Promise((resolve) => {
13
+ // `detached` lets a timeout SIGKILL the whole process group (npm fans out to
14
+ // node/esbuild/vitest workers that an immediate-child kill would orphan).
15
+ const child = spawn(cmd, [...args], { cwd, stdio: ['ignore', 'pipe', 'pipe'], detached: true });
16
+ let buf = '';
17
+ const onData = (d) => {
18
+ buf += d.toString('utf8');
19
+ if (buf.length > MAX_CMD_OUTPUT_BYTES)
20
+ buf = buf.slice(buf.length - MAX_CMD_OUTPUT_BYTES);
21
+ };
22
+ child.stdout.on('data', onData);
23
+ child.stderr.on('data', onData);
24
+ const timer = setTimeout(() => killTree(child), CMD_TIMEOUT_MS);
25
+ let settled = false;
26
+ const finish = (r) => {
27
+ if (settled)
28
+ return;
29
+ settled = true;
30
+ clearTimeout(timer);
31
+ child.stdout.off('data', onData);
32
+ child.stderr.off('data', onData);
33
+ resolve(r);
34
+ };
35
+ child.on('error', (err) => finish({ exitCode: -1, output: `${cmd} failed to start: ${err.message}` }));
36
+ child.on('close', (code) => finish({ exitCode: code ?? -1, output: buf }));
37
+ });
38
+ }
39
+ /** SIGKILL a detached child's whole process group, tolerating an already-dead group. */
40
+ function killTree(child) {
41
+ try {
42
+ if (child.pid != null)
43
+ process.kill(-child.pid, 'SIGKILL');
44
+ else
45
+ child.kill('SIGKILL');
46
+ }
47
+ catch {
48
+ try {
49
+ child.kill('SIGKILL');
50
+ }
51
+ catch {
52
+ /* already gone */
53
+ }
54
+ }
55
+ }
56
+ function truncate(s, n = 1200) {
57
+ return s.length <= n ? s : `…${s.slice(s.length - n)}`;
58
+ }
59
+ async function readPkg(dir) {
60
+ try {
61
+ return JSON.parse(await fs.readFile(path.join(dir, 'package.json'), 'utf8'));
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ }
67
+ const PM_COMMANDS = {
68
+ npm: { bin: 'npm', install: ['install', '--no-fund', '--no-audit'], run: (s) => ['run', s], test: ['test'] },
69
+ pnpm: { bin: 'pnpm', install: ['install'], run: (s) => ['run', s], test: ['test'] },
70
+ yarn: { bin: 'yarn', install: ['install'], run: (s) => ['run', s], test: ['test'] },
71
+ bun: { bin: 'bun', install: ['install'], run: (s) => ['run', s], test: ['test'] },
72
+ };
73
+ /**
74
+ * Pick the package manager from the npm-standard `packageManager` field
75
+ * ("pnpm@9", "yarn@4", …) so a non-npm plugin can declare its toolchain. The
76
+ * default is npm, preserving prior behavior for plugins that omit the field or
77
+ * name an unknown manager.
78
+ */
79
+ function pmFor(pkg) {
80
+ const name = (pkg.packageManager ?? '').split('@')[0]?.trim().toLowerCase();
81
+ return (name && PM_COMMANDS[name]) || PM_COMMANDS.npm;
82
+ }
83
+ /**
84
+ * Run a plugin's own `build` and `test` npm scripts when present. A plugin
85
+ * scaffolded as a zero-build `.mjs` (no scripts) passes straight through —
86
+ * jiti / dynamic-import handles it at reload time, which is the real load test.
87
+ */
88
+ export async function verifyPluginBuild(target) {
89
+ const results = [];
90
+ const pkg = await readPkg(target.path);
91
+ if (!pkg)
92
+ return results; // no package.json (e.g. bare .mjs) → nothing to build
93
+ const pm = pmFor(pkg);
94
+ // Install deps only if the plugin declares any and they aren't present yet.
95
+ if (pkg.dependencies && Object.keys(pkg.dependencies).length > 0) {
96
+ const hasModules = await fs
97
+ .access(path.join(target.path, 'node_modules'))
98
+ .then(() => true)
99
+ .catch(() => false);
100
+ if (!hasModules) {
101
+ const r = await runCmd(pm.bin, pm.install, target.path);
102
+ results.push({
103
+ stage: 'install',
104
+ ok: r.exitCode === 0,
105
+ message: r.exitCode === 0 ? 'deps installed' : truncate(r.output),
106
+ });
107
+ if (r.exitCode !== 0)
108
+ return results;
109
+ }
110
+ }
111
+ if (pkg.scripts?.build) {
112
+ const r = await runCmd(pm.bin, pm.run('build'), target.path);
113
+ results.push({
114
+ stage: 'build',
115
+ ok: r.exitCode === 0,
116
+ message: r.exitCode === 0 ? 'build ok' : truncate(r.output),
117
+ });
118
+ if (r.exitCode !== 0)
119
+ return results;
120
+ }
121
+ if (pkg.scripts?.test) {
122
+ const r = await runCmd(pm.bin, pm.test, target.path);
123
+ results.push({
124
+ stage: 'test',
125
+ ok: r.exitCode === 0,
126
+ message: r.exitCode === 0 ? 'tests ok' : truncate(r.output),
127
+ });
128
+ }
129
+ return results;
130
+ }
131
+ /**
132
+ * Require a real (non-empty, non-blank-quoted) value after a frontmatter key.
133
+ * Rejects `name:`, `name: ""`, `name: ' '` — a quote alone satisfied the old
134
+ * `\S+`, accepting a structurally-present-but-empty field.
135
+ */
136
+ const FRONTMATTER_FIELD = (key) => new RegExp(`^${key}:[ \\t]*(?:"[ \\t]*[^"\\s][^"]*"|'[ \\t]*[^'\\s][^']*'|[^"'\\s].*)$`, 'm');
137
+ /** Minimal structural check that a skill file has the required frontmatter. */
138
+ export async function verifySkillFile(target) {
139
+ let raw;
140
+ try {
141
+ raw = await fs.readFile(target.path, 'utf8');
142
+ }
143
+ catch {
144
+ return { stage: 'parse', ok: false, message: `skill file not found: ${target.path}` };
145
+ }
146
+ // Normalize CRLF so a Windows-authored skill isn't rejected by the \n anchors.
147
+ const normalized = raw.replace(/\r\n/g, '\n');
148
+ const fm = normalized.match(/^---\n([\s\S]*?)\n---/);
149
+ if (!fm) {
150
+ return { stage: 'parse', ok: false, message: 'missing YAML frontmatter block (--- … ---)' };
151
+ }
152
+ const body = fm[1] ?? '';
153
+ const hasName = FRONTMATTER_FIELD('name').test(body);
154
+ const hasDesc = FRONTMATTER_FIELD('description').test(body);
155
+ if (!hasName || !hasDesc) {
156
+ return {
157
+ stage: 'parse',
158
+ ok: false,
159
+ message: 'frontmatter must declare both a non-empty `name:` and `description:`',
160
+ };
161
+ }
162
+ return { stage: 'parse', ok: true, message: 'frontmatter ok' };
163
+ }
164
+ //# sourceMappingURL=verify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@moxxy/plugin-self-update",
3
+ "version": "0.26.0",
4
+ "description": "Model-callable self-update: guardrailed, transactional editing of moxxy's own plugins/skills (Tier 1, hot-reloaded) and core packages (Tier 2, build+overlay+restart). Every change is approval-gated, verified, and auto-rolled-back on failure. Disable to lock the code base.",
5
+ "keywords": [
6
+ "moxxy",
7
+ "agent",
8
+ "self-update",
9
+ "tools"
10
+ ],
11
+ "homepage": "https://moxxy.ai",
12
+ "bugs": {
13
+ "url": "https://github.com/moxxy-ai/moxxy/issues"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/moxxy-ai/moxxy.git",
18
+ "directory": "packages/plugin-self-update"
19
+ },
20
+ "author": "Michal Makowski <michal.makowski97@gmail.com>",
21
+ "license": "MIT",
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "type": "module",
26
+ "main": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js"
32
+ }
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "src"
37
+ ],
38
+ "moxxy": {
39
+ "plugin": {
40
+ "entry": "./dist/index.js",
41
+ "kind": "tools"
42
+ }
43
+ },
44
+ "dependencies": {
45
+ "zod": "^3.24.0",
46
+ "@moxxy/sdk": "0.26.0"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^22.10.0",
50
+ "typescript": "^5.7.3",
51
+ "vitest": "^2.1.8",
52
+ "@moxxy/tsconfig": "0.0.0",
53
+ "@moxxy/vitest-preset": "0.0.0"
54
+ },
55
+ "scripts": {
56
+ "build": "tsc -p tsconfig.json",
57
+ "typecheck": "tsc -p tsconfig.json --noEmit",
58
+ "test": "vitest run",
59
+ "clean": "rm -rf dist .turbo"
60
+ }
61
+ }
@@ -0,0 +1,176 @@
1
+ import type {
2
+ EventLogReader,
3
+ MoxxyEvent,
4
+ MoxxyEventOfType,
5
+ MoxxyEventType,
6
+ TurnId,
7
+ } from '@moxxy/sdk';
8
+ import { describe, expect, it } from 'vitest';
9
+ import { classify, gatherSignals, suggestName, type ClassifySignals } from './classify.js';
10
+
11
+ const empty: ClassifySignals = { failedTools: [], errorMessages: [], registeredTools: [] };
12
+
13
+ // Minimal EventLogReader backed by a plain array — gatherSignals only uses
14
+ // `length`, `slice` and `ofType`, so we stub just those faithfully.
15
+ function reader(events: ReadonlyArray<MoxxyEvent>): EventLogReader {
16
+ return {
17
+ length: events.length,
18
+ at: (seq) => events[seq],
19
+ slice: (from = 0, to = events.length) => events.slice(from, to),
20
+ ofType: <T extends MoxxyEventType>(type: T): ReadonlyArray<MoxxyEventOfType<T>> =>
21
+ events.filter((e): e is MoxxyEventOfType<T> => e.type === type),
22
+ byTurn: (turnId: TurnId) => events.filter((e) => e.turnId === turnId),
23
+ toJSON: () => events,
24
+ };
25
+ }
26
+
27
+ function ev(seq: number, partial: Record<string, unknown>): MoxxyEvent {
28
+ return {
29
+ id: `e${seq}`,
30
+ seq,
31
+ ts: seq,
32
+ sessionId: 'sess',
33
+ turnId: 'turn',
34
+ source: 'system',
35
+ ...partial,
36
+ } as unknown as MoxxyEvent;
37
+ }
38
+
39
+ describe('classify', () => {
40
+ it('escalates to core when the error mentions core internals', () => {
41
+ const r = classify(
42
+ { trigger: 'error' },
43
+ { ...empty, errorMessages: ['TypeError in packages/core/src/run-turn.ts'] },
44
+ );
45
+ expect(r.tier).toBe('core');
46
+ });
47
+
48
+ it('recommends a plugin when a called tool is not registered', () => {
49
+ const r = classify(
50
+ { trigger: 'error' },
51
+ { failedTools: ['fetch_weather'], errorMessages: ['no tool'], registeredTools: ['Read', 'Bash'] },
52
+ );
53
+ expect(r.tier).toBe('plugin');
54
+ expect(r.candidateName).toBeDefined();
55
+ });
56
+
57
+ it('recommends a skill when an existing tool was misused', () => {
58
+ const r = classify(
59
+ { trigger: 'error', text: 'it keeps using the wrong path' },
60
+ { failedTools: ['Read'], errorMessages: ['ENOENT'], registeredTools: ['Read'] },
61
+ );
62
+ expect(r.tier).toBe('skill');
63
+ });
64
+
65
+ it('recommends a plugin to wrap an existing misbehaving tool when override is implied', () => {
66
+ const r = classify(
67
+ { trigger: 'request', text: 'wrap the Read tool to truncate huge files' },
68
+ { failedTools: ['Read'], errorMessages: [], registeredTools: ['Read'] },
69
+ );
70
+ expect(r.tier).toBe('plugin');
71
+ });
72
+
73
+ it('treats a procedure request as a skill', () => {
74
+ const r = classify(
75
+ { trigger: 'request', text: 'always run the linter before committing' },
76
+ empty,
77
+ );
78
+ expect(r.tier).toBe('skill');
79
+ });
80
+
81
+ it('treats a new-capability request as a plugin', () => {
82
+ const r = classify(
83
+ { trigger: 'request', text: 'add a tool that calls the GitHub API' },
84
+ empty,
85
+ );
86
+ expect(r.tier).toBe('plugin');
87
+ });
88
+
89
+ it('defaults ambiguous requests to the plugin tier', () => {
90
+ const r = classify({ trigger: 'request', text: 'do the thing' }, empty);
91
+ expect(r.tier).toBe('plugin');
92
+ });
93
+ });
94
+
95
+ describe('suggestName', () => {
96
+ it('builds a kebab slug, dropping stop words', () => {
97
+ expect(suggestName('add a tool that calls the GitHub API')).toBe('tool-calls-github-api');
98
+ });
99
+ it('returns undefined for empty input', () => {
100
+ expect(suggestName(undefined)).toBeUndefined();
101
+ });
102
+ });
103
+
104
+ describe('gatherSignals', () => {
105
+ it('correlates failed tool_result events back to their call name', () => {
106
+ const events = [
107
+ ev(0, { type: 'tool_call_requested', callId: 'c1', name: 'fetch_weather', input: {} }),
108
+ ev(1, {
109
+ type: 'tool_result',
110
+ callId: 'c1',
111
+ ok: false,
112
+ error: { message: 'boom', kind: 'threw' },
113
+ }),
114
+ ];
115
+ const sig = gatherSignals(reader(events), ['Read']);
116
+ expect(sig.failedTools).toEqual(['fetch_weather']);
117
+ expect(sig.errorMessages).toEqual(['boom']);
118
+ expect(sig.registeredTools).toEqual(['Read']);
119
+ });
120
+
121
+ it('ignores successful results and collects standalone error events', () => {
122
+ const events = [
123
+ ev(0, { type: 'tool_call_requested', callId: 'c1', name: 'Read', input: {} }),
124
+ ev(1, { type: 'tool_result', callId: 'c1', ok: true, output: 'fine' }),
125
+ ev(2, { type: 'error', message: 'session-level failure' }),
126
+ ];
127
+ const sig = gatherSignals(reader(events), []);
128
+ expect(sig.failedTools).toEqual([]);
129
+ expect(sig.errorMessages).toEqual(['session-level failure']);
130
+ });
131
+
132
+ it('records the error message for a failed-but-unregistered call name', () => {
133
+ // A tool_result whose callId has no matching tool_call_requested still
134
+ // contributes its error message, but cannot name a failed tool.
135
+ const events = [
136
+ ev(0, {
137
+ type: 'tool_result',
138
+ callId: 'orphan',
139
+ ok: false,
140
+ error: { message: 'no request seen', kind: 'threw' },
141
+ }),
142
+ ];
143
+ const sig = gatherSignals(reader(events), []);
144
+ expect(sig.failedTools).toEqual([]);
145
+ expect(sig.errorMessages).toEqual(['no request seen']);
146
+ });
147
+
148
+ it('only inspects events within the lookback window', () => {
149
+ // The request is far in the past; with a tiny lookback only the recent
150
+ // failing result is scanned. The callName map is still built from the
151
+ // whole log, so the correlation resolves even though the request itself
152
+ // is outside the window.
153
+ const events = [
154
+ ev(0, { type: 'tool_call_requested', callId: 'c1', name: 'old_tool', input: {} }),
155
+ ev(1, { type: 'tool_result', callId: 'c1', ok: false, error: { message: 'stale', kind: 'threw' } }),
156
+ ev(2, { type: 'tool_call_requested', callId: 'c2', name: 'new_tool', input: {} }),
157
+ ev(3, { type: 'tool_result', callId: 'c2', ok: false, error: { message: 'fresh', kind: 'threw' } }),
158
+ ];
159
+ const sig = gatherSignals(reader(events), [], 2);
160
+ // Only seq 2..3 are in the window, so only new_tool's failure is recorded.
161
+ expect(sig.failedTools).toEqual(['new_tool']);
162
+ expect(sig.errorMessages).toEqual(['fresh']);
163
+ });
164
+
165
+ it('dedupes a tool that fails repeatedly', () => {
166
+ const events = [
167
+ ev(0, { type: 'tool_call_requested', callId: 'c1', name: 'flaky', input: {} }),
168
+ ev(1, { type: 'tool_result', callId: 'c1', ok: false, error: { message: 'e1', kind: 'threw' } }),
169
+ ev(2, { type: 'tool_call_requested', callId: 'c2', name: 'flaky', input: {} }),
170
+ ev(3, { type: 'tool_result', callId: 'c2', ok: false, error: { message: 'e2', kind: 'threw' } }),
171
+ ];
172
+ const sig = gatherSignals(reader(events), []);
173
+ expect(sig.failedTools).toEqual(['flaky']);
174
+ expect(sig.errorMessages).toEqual(['e1', 'e2']);
175
+ });
176
+ });