@platless/axom-cli 0.1.0 → 0.3.13

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 (109) hide show
  1. package/README.md +1 -1
  2. package/dist/authScopes.js +19 -0
  3. package/dist/axom-patch/cli/README.md +110 -0
  4. package/dist/axom-patch/cli/pi-steering.json +37 -0
  5. package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +34 -0
  6. package/dist/axom-patch/cli/probes/pi-steering.mjs +80 -0
  7. package/dist/axom-patch/cli/probes/qoder-steering.mjs +89 -0
  8. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +49 -0
  9. package/dist/axom-patch/cli/qoder-background-terminal.md +81 -0
  10. package/dist/axom-patch/cli/qoder-foreground-output.md +41 -0
  11. package/dist/axom-patch/cli/qoder-steering.json +52 -0
  12. package/dist/axom-patch/cli/qoder-steering.md +30 -0
  13. package/dist/axom-patch/cli/qoder-subagent-origin.json +73 -0
  14. package/dist/axom-patch/cli/qoder-subagent-subwork.md +73 -0
  15. package/dist/axom-patch/cli/runtime.json +37 -0
  16. package/dist/axom-patch/cli/steering/claude-steering-js.patch +89 -0
  17. package/dist/axom-patch/cli/steering/claude-steering-types.patch +28 -0
  18. package/dist/axom-patch/cli/steering/codex-steering.patch +57 -0
  19. package/dist/axom-patch/cli/steering/pi-steering.js +79 -0
  20. package/dist/cli.js +9 -1
  21. package/dist/commands/auth.js +4 -13
  22. package/dist/commands/execute.js +4 -4
  23. package/dist/commands/record.js +6 -6
  24. package/dist/commands/struct.js +4 -4
  25. package/dist/commands/workstation.js +2 -2
  26. package/dist/credentials.js +2 -2
  27. package/dist/install/claude/commands.js +13 -13
  28. package/dist/mcp.js +32 -14
  29. package/dist/workstation/acp/acpAgents.js +447 -166
  30. package/dist/workstation/acp/acpClient.js +232 -18
  31. package/dist/workstation/acp/acpSession.js +502 -74
  32. package/dist/workstation/acp/activityBusy.js +43 -0
  33. package/dist/workstation/acp/activityReporting.js +54 -0
  34. package/dist/workstation/acp/adoption.js +107 -49
  35. package/dist/workstation/acp/authUrlSniffer.js +36 -0
  36. package/dist/workstation/acp/contract.js +11 -6
  37. package/dist/workstation/acp/delegationMcp.js +24 -0
  38. package/dist/workstation/acp/draftInterceptor.js +9 -0
  39. package/dist/workstation/acp/managedPatches.js +228 -0
  40. package/dist/workstation/acp/patchAssets.js +63 -0
  41. package/dist/workstation/acp/permission.js +300 -34
  42. package/dist/workstation/acp/qoderStartup.js +45 -0
  43. package/dist/workstation/acp/registry/catalog.js +263 -4
  44. package/dist/workstation/acp/registry/detect.js +55 -0
  45. package/dist/workstation/acp/sessionScope.js +19 -0
  46. package/dist/workstation/acp/sessionUpdateMapper.js +39 -21
  47. package/dist/workstation/acp/terminalLogin.js +197 -0
  48. package/dist/workstation/acp/test/fakeAgent.js +185 -9
  49. package/dist/workstation/acp/validate.js +33 -5
  50. package/dist/workstation/agentSessions.js +532 -44
  51. package/dist/workstation/channels/agent.js +7 -1
  52. package/dist/workstation/channels/computer.js +152 -4
  53. package/dist/workstation/channels/management.js +3 -3
  54. package/dist/workstation/config.js +3 -1
  55. package/dist/workstation/connection/encryptedChannel.js +88 -21
  56. package/dist/workstation/daemon.js +25 -3
  57. package/dist/workstation/desktop/binding.js +36 -0
  58. package/dist/workstation/desktop/bridge.js +212 -0
  59. package/dist/workstation/desktop/flow-control.js +52 -0
  60. package/dist/workstation/desktop/focused-text.js +130 -0
  61. package/dist/workstation/desktop/protocol.js +35 -0
  62. package/dist/workstation/fileOps.js +277 -0
  63. package/dist/workstation/filePaths.js +174 -0
  64. package/dist/workstation/fileWorker.js +81 -0
  65. package/dist/workstation/fileWorkerClient.js +121 -0
  66. package/dist/workstation/oauth/deviceAuthorization.js +21 -12
  67. package/dist/workstation/protocol/agent.js +154 -5
  68. package/dist/workstation/protocol/common.js +6 -0
  69. package/dist/workstation/protocol/computer.js +92 -3
  70. package/dist/workstation/qoderAdapter/backgroundProcess.js +106 -0
  71. package/dist/workstation/qoderAdapter/backgroundTerminal.js +262 -0
  72. package/dist/workstation/qoderAdapter/completedContinuation.js +139 -0
  73. package/dist/workstation/qoderAdapter/contract.js +78 -0
  74. package/dist/workstation/qoderAdapter/foregroundOutput.js +30 -0
  75. package/dist/workstation/qoderAdapter/history.js +87 -0
  76. package/dist/workstation/qoderAdapter/identityIndex.js +191 -0
  77. package/dist/workstation/qoderAdapter/inputQueue.js +42 -0
  78. package/dist/workstation/qoderAdapter/main.js +93 -0
  79. package/dist/workstation/qoderAdapter/mapper.js +117 -0
  80. package/dist/workstation/qoderAdapter/observerPlugin.js +245 -0
  81. package/dist/workstation/qoderAdapter/observerReceiver.js +187 -0
  82. package/dist/workstation/qoderAdapter/permissions.js +61 -0
  83. package/dist/workstation/qoderAdapter/profile.js +93 -0
  84. package/dist/workstation/qoderAdapter/sdkDriver.js +49 -0
  85. package/dist/workstation/qoderAdapter/session.js +319 -0
  86. package/dist/workstation/qoderAdapter/subagentMeta.js +28 -0
  87. package/dist/workstation/qoderAdapter/subwork.js +77 -0
  88. package/dist/workstation/qoderAdapter/taskControl.js +38 -0
  89. package/dist/workstation/qoderAdapter/taskEvent.js +26 -0
  90. package/dist/workstation/qoderAdapter/taskLedger.js +163 -0
  91. package/dist/workstation/qoderAdapter/types.js +1 -0
  92. package/dist/workstation/qoderAdapter/usage.js +37 -0
  93. package/dist/workstation/register.js +4 -0
  94. package/dist/workstation/tools/browser.js +394 -0
  95. package/dist/workstation/tools/client.js +269 -0
  96. package/dist/workstation/tools/core.js +50 -0
  97. package/dist/workstation/tools/files.js +525 -0
  98. package/dist/workstation/tools/keeperProcess.js +114 -0
  99. package/dist/workstation/tools/terminal.js +214 -0
  100. package/dist/workstation/tools/validation.js +50 -0
  101. package/dist/workstation/update/coordinator.js +29 -0
  102. package/dist/workstation/update/npmInstaller.js +2 -0
  103. package/dist/workstation/update/ptyPreflight.js +24 -0
  104. package/dist/workstation/update/taskRegistry.js +44 -20
  105. package/dist/workstation/update/watchdog.js +66 -0
  106. package/package.json +14 -4
  107. package/skills/axom/SKILL.md +55 -17
  108. package/skills/axom-definition/SKILL.md +32 -32
  109. package/skills/axom-platform/SKILL.md +39 -39
@@ -0,0 +1,228 @@
1
+ /** Automatic adapter patches, owned by Axom, never written into npm's shared cache. */
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { cp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { installNpxPackage, npmCacheDir, npmPackageName, npmPackageVersion } from './installProbe.js';
6
+ import { packagePatchMatch, packagePatches } from './patchAssets.js';
7
+ const sha = (content) => createHash('sha256').update(content).digest('hex');
8
+ const pending = new Map();
9
+ const warned = new Set();
10
+ function warnOnce(key, message) {
11
+ if (warned.has(key))
12
+ return;
13
+ warned.add(key);
14
+ console.warn(message);
15
+ }
16
+ function inside(root, relative) {
17
+ const target = path.resolve(root, relative);
18
+ if (!target.startsWith(path.resolve(root) + path.sep))
19
+ throw new Error('Axom patch path escapes its package');
20
+ return target;
21
+ }
22
+ /** A matching version and exact original bytes; already-patched or changed originals are rejected. */
23
+ export async function applyPackagePatches(packageDir, recipes) {
24
+ const output = {};
25
+ for (const recipe of recipes) {
26
+ const metadata = JSON.parse(await readFile(path.join(packageDir, 'package.json'), 'utf8'));
27
+ if (metadata.name !== recipe.package || !packagePatchMatch(recipe, metadata.version)) {
28
+ throw new Error(`Axom patch '${recipe.id}' does not support ${metadata.name}@${metadata.version}`);
29
+ }
30
+ for (const edit of recipe.edits) {
31
+ const filename = inside(packageDir, edit.file);
32
+ const original = await readFile(filename, 'utf8');
33
+ if (sha(original) !== edit.before_sha256 || !edit.before || original.split(edit.before).length !== 2) {
34
+ throw new Error(`Axom patch '${recipe.id}': original bytes changed in ${edit.file}`);
35
+ }
36
+ const patched = original.replace(edit.before, edit.after);
37
+ if (sha(patched) !== edit.after_sha256)
38
+ throw new Error(`Axom patch '${recipe.id}': invalid expected output`);
39
+ await writeFile(filename, patched, 'utf8');
40
+ output[edit.file] = sha(patched);
41
+ }
42
+ }
43
+ return output;
44
+ }
45
+ /** Whether every edit of `recipe` finds the bytes it was written against, edits on one file applied in order. */
46
+ async function originalsFit(packageDir, recipe) {
47
+ const files = new Map();
48
+ for (const edit of recipe.edits) {
49
+ let content = files.get(edit.file);
50
+ if (content === undefined) {
51
+ try {
52
+ content = await readFile(inside(packageDir, edit.file), 'utf8');
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ }
58
+ if (sha(content) !== edit.before_sha256 || !edit.before || content.split(edit.before).length !== 2)
59
+ return false;
60
+ files.set(edit.file, content.replace(edit.before, edit.after));
61
+ }
62
+ return true;
63
+ }
64
+ async function cachedModules(name, version, probe) {
65
+ const root = path.join(npmCacheDir(probe), '_npx');
66
+ let entries;
67
+ try {
68
+ entries = await readdir(root, { withFileTypes: true });
69
+ }
70
+ catch {
71
+ return undefined;
72
+ }
73
+ for (const entry of entries) {
74
+ if (!entry.isDirectory())
75
+ continue;
76
+ const modules = path.join(root, entry.name, 'node_modules');
77
+ try {
78
+ const metadata = JSON.parse(await readFile(inside(modules, name + '/package.json'), 'utf8'));
79
+ if (metadata.name === name && metadata.version === version)
80
+ return modules;
81
+ }
82
+ catch { /* Interrupted/other version of npm's cache. */ }
83
+ }
84
+ return undefined;
85
+ }
86
+ /** Called by Install/Update and every real launch, including adapters installed before this CLI. */
87
+ export async function preparePatchedLaunch(agent, launch, adapterRoot, probe, options = {}) {
88
+ const distribution = agent.distribution.npx;
89
+ if (!distribution)
90
+ return launch;
91
+ const name = npmPackageName(distribution.package);
92
+ const version = npmPackageVersion(distribution.package);
93
+ const candidates = (options.recipes ?? packagePatches).filter((patch) => patch.package === name);
94
+ const matched = candidates.flatMap((recipe) => {
95
+ const match = version ? packagePatchMatch(recipe, version) : undefined;
96
+ return match ? [{ recipe, match }] : [];
97
+ });
98
+ if (matched.length && (!/^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/i.test(name) || !version || !/^[0-9a-z.+-]+$/i.test(version))) {
99
+ throw new Error('Invalid package identity for Axom adapter patch');
100
+ }
101
+ // A range only says a recipe may fit; the pristine package's bytes decide.
102
+ const fitting = new Set();
103
+ if (matched.some(({ match }) => match === 'range')) {
104
+ const modules = await pristineModules();
105
+ for (const { recipe, match } of matched) {
106
+ if (match !== 'range')
107
+ continue;
108
+ if (await originalsFit(inside(modules, name), recipe))
109
+ fitting.add(recipe);
110
+ else
111
+ warnOnce(`${recipe.id}@${distribution.package}`, `[axom-patch] ${recipe.id} does not fit ${distribution.package}; using the upstream package. Review the patch inventory.`);
112
+ }
113
+ }
114
+ const recipes = matched.filter(({ recipe, match }) => match === 'exact' || fitting.has(recipe)).map(({ recipe }) => recipe);
115
+ if (!recipes.length) {
116
+ if (candidates.length && !matched.length) {
117
+ warnOnce(distribution.package, `[axom-patch] No verified patch for ${distribution.package}; using the upstream package. Review the patch inventory.`);
118
+ }
119
+ return launch;
120
+ }
121
+ const digest = sha(JSON.stringify(recipes));
122
+ const target = path.join(adapterRoot, 'patched-npm', sha(name).slice(0, 16), version, digest);
123
+ const existing = pending.get(target);
124
+ if (existing) {
125
+ await existing;
126
+ return makeLaunch();
127
+ }
128
+ const preparation = prepare();
129
+ pending.set(target, preparation);
130
+ try {
131
+ await preparation;
132
+ return await makeLaunch();
133
+ }
134
+ finally {
135
+ pending.delete(target);
136
+ }
137
+ async function prepare() {
138
+ const packageDir = inside(path.join(target, 'node_modules'), name);
139
+ const receiptFile = path.join(target, 'axom-patches.json');
140
+ let receipt;
141
+ try {
142
+ receipt = JSON.parse(await readFile(receiptFile, 'utf8'));
143
+ }
144
+ catch (error) {
145
+ if (error.code !== 'ENOENT')
146
+ throw error;
147
+ }
148
+ if (receipt) {
149
+ if (receipt.digest !== digest)
150
+ throw new Error('Axom adapter patch receipt does not match this CLI');
151
+ // Derive expected files from the shipped recipe, never from a writable receipt alone.
152
+ const expected = Object.fromEntries(recipes.flatMap((recipe) => recipe.edits.map((edit) => [edit.file, edit.after_sha256])));
153
+ if (JSON.stringify(receipt.files) !== JSON.stringify(expected))
154
+ throw new Error('Incomplete Axom adapter patch receipt');
155
+ for (const [file, hash] of Object.entries(expected)) {
156
+ if (sha(await readFile(inside(packageDir, file))) !== hash)
157
+ throw new Error(`Axom patched adapter was modified: ${file}`);
158
+ }
159
+ }
160
+ else {
161
+ try {
162
+ await stat(target);
163
+ throw new Error('Incomplete Axom patched adapter directory; refusing to use it');
164
+ }
165
+ catch (error) {
166
+ if (error.code !== 'ENOENT')
167
+ throw error;
168
+ }
169
+ const modules = await pristineModules();
170
+ await mkdir(path.dirname(target), { recursive: true });
171
+ const temporary = target + '.staging-' + randomUUID();
172
+ try {
173
+ // Dependencies and .bin are part of the adapter environment; clone them as well.
174
+ await cp(modules, path.join(temporary, 'node_modules'), { recursive: true, dereference: true });
175
+ const files = await applyPackagePatches(inside(path.join(temporary, 'node_modules'), name), recipes);
176
+ await writeFile(path.join(temporary, 'axom-patches.json'), JSON.stringify({
177
+ digest, package: name, version, patches: recipes.map((recipe) => recipe.id), files,
178
+ }) + '\n');
179
+ try {
180
+ await rename(temporary, target);
181
+ }
182
+ catch (error) {
183
+ // A second daemon may have published the same immutable copy while we built ours.
184
+ if (!['EEXIST', 'ENOTEMPTY'].includes(error.code ?? ''))
185
+ throw error;
186
+ return prepare();
187
+ }
188
+ console.info(`[axom-patch] Applied ${recipes.map((recipe) => recipe.id).join(', ')} to ${name}@${version}`);
189
+ }
190
+ finally {
191
+ await rm(temporary, { recursive: true, force: true });
192
+ }
193
+ }
194
+ }
195
+ async function pristineModules() {
196
+ let modules = await cachedModules(name, version, probe);
197
+ if (!modules) {
198
+ const installed = await installNpxPackage(distribution.package, {
199
+ env: probe, timeoutMs: options.timeoutMs, npxCommand: options.npxCommand,
200
+ });
201
+ if (!installed.ok)
202
+ throw new Error(installed.error);
203
+ modules = await cachedModules(name, version, probe);
204
+ }
205
+ if (!modules)
206
+ throw new Error(`Installed package ${distribution.package} is absent from npm's cache`);
207
+ return modules;
208
+ }
209
+ async function makeLaunch() {
210
+ const packageDir = inside(path.join(target, 'node_modules'), name);
211
+ const metadata = JSON.parse(await readFile(path.join(packageDir, 'package.json'), 'utf8'));
212
+ if (metadata.name !== name || metadata.version !== version)
213
+ throw new Error('Patched adapter package identity changed');
214
+ const selectedBins = [...new Set(recipes.map(recipe => recipe.bin).filter((bin) => bin !== undefined))];
215
+ if (selectedBins.length > 1)
216
+ throw new Error('Axom patches disagree about the adapter executable');
217
+ const binaries = selectedBins.length ? [metadata.bin?.[selectedBins[0]]] :
218
+ typeof metadata.bin === 'string' ? [metadata.bin] : Object.values(metadata.bin ?? {});
219
+ if (binaries.length !== 1 || typeof binaries[0] !== 'string')
220
+ throw new Error('Patched adapter has no unambiguous executable');
221
+ const entry = inside(packageDir, binaries[0]);
222
+ if (!(await stat(entry)).isFile())
223
+ throw new Error('Patched adapter executable is missing');
224
+ const env = { ...launch.env };
225
+ env.PATH = path.join(target, 'node_modules', '.bin') + path.delimiter + (env.PATH ?? probe.path ?? process.env.PATH ?? '');
226
+ return { command: process.execPath, args: [entry, ...(distribution.args ?? [])], env };
227
+ }
228
+ }
@@ -0,0 +1,63 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import semver from 'semver';
4
+ // Production reads only assets shipped in this CLI. The .ts path is for source-based tests.
5
+ export const patchAssetRoot = new URL(import.meta.url.endsWith('.ts') ? '../../../../axom-patch/cli/' : '../../axom-patch/cli/', import.meta.url);
6
+ const runtime = JSON.parse(readFileSync(new URL('runtime.json', patchAssetRoot), 'utf8'));
7
+ if (runtime.schema_version !== 1)
8
+ throw new Error('Unsupported bundled Axom patch schema');
9
+ for (const entry of runtime.overrides)
10
+ validateOverride(entry);
11
+ function validateCondition(id, when, until, whenKey, untilKey) {
12
+ if (when !== undefined && until !== undefined)
13
+ throw new Error(`Axom patch ${id}: ${whenKey} and ${untilKey} are exclusive`);
14
+ if (when !== undefined && (!Array.isArray(when) || when.length === 0 || when.some((version) => !semver.valid(version)))) {
15
+ throw new Error(`Axom patch ${id}: ${whenKey} must list exact versions`);
16
+ }
17
+ if (until !== undefined && !semver.valid(until))
18
+ throw new Error(`Axom patch ${id}: ${untilKey} is not a version`);
19
+ }
20
+ export function validateOverride(entry) {
21
+ validateCondition(entry.id, entry.when_registry_version, entry.until_registry_version, 'when_registry_version', 'until_registry_version');
22
+ const until = entry.until_registry_version;
23
+ // A pin at or past its own bound would be replaced by the registry the moment it applied.
24
+ if (until !== undefined && entry.distribution && !(semver.valid(entry.distribution.version) && semver.lt(entry.distribution.version, until))) {
25
+ throw new Error(`Axom patch ${entry.id}: pinned ${entry.distribution.version} must sort below ${until}`);
26
+ }
27
+ }
28
+ export function validatePackagePatch(recipe) {
29
+ validateCondition(recipe.id, recipe.versions, recipe.until_version, 'versions', 'until_version');
30
+ if (recipe.versions === undefined && recipe.until_version === undefined) {
31
+ throw new Error(`Axom patch ${recipe.id}: needs versions or until_version`);
32
+ }
33
+ }
34
+ function inRange(version, when, until) {
35
+ if (when)
36
+ return semver.valid(version) !== null && when.some((listed) => semver.eq(listed, version));
37
+ if (until)
38
+ return !(semver.valid(version) && semver.gte(version, until));
39
+ return true;
40
+ }
41
+ export function overrideApplies(entry, registryVersion) {
42
+ return inRange(registryVersion, entry.when_registry_version, entry.until_registry_version);
43
+ }
44
+ /** Exact versions are a promise that the bytes match; a range only says the recipe may fit. */
45
+ export function packagePatchMatch(recipe, version) {
46
+ if (recipe.versions)
47
+ return recipe.versions.includes(version) ? 'exact' : undefined;
48
+ return inRange(version, undefined, recipe.until_version) ? 'range' : undefined;
49
+ }
50
+ /** The overrides that apply to `agent` at `registryVersion`, merged: the first distribution wins, env merges. */
51
+ export function patchOverridesFor(agent, registryVersion, overrides = runtime.overrides) {
52
+ const matching = overrides.filter((entry) => entry.agent === agent && overrideApplies(entry, registryVersion));
53
+ const distribution = matching.find((entry) => entry.distribution)?.distribution;
54
+ const env = Object.assign({}, ...matching.map((entry) => entry.env ?? {}));
55
+ return { ...(distribution && { distribution }), ...(Object.keys(env).length && { env }) };
56
+ }
57
+ export const packagePatches = runtime.package_patches.map((file) => {
58
+ if (!/^[a-z0-9-]+\.json$/.test(file))
59
+ throw new Error('Invalid bundled Axom patch filename');
60
+ const recipe = JSON.parse(readFileSync(fileURLToPath(new URL(file, patchAssetRoot)), 'utf8'));
61
+ validatePackagePatch(recipe);
62
+ return recipe;
63
+ });