@north-light/crouter 0.3.163 → 0.3.165
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-root.d.ts +6 -6
- package/dist/build-root.js +12 -13
- package/dist/builtin-memory/00-runtime-base.md +3 -4
- package/dist/builtin-memory/01-spine/01-no-manager.md +1 -1
- package/dist/builtin-memory/02-lifecycle/00-terminal.md +2 -0
- package/dist/builtin-memory/04-orchestration-kernel.md +16 -18
- package/dist/builtin-memory/05-kinds/advisor/00-base.md +0 -2
- package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +3 -9
- package/dist/builtin-memory/05-kinds/developer/00-base.md +1 -3
- package/dist/builtin-memory/05-kinds/developer/01-orchestrator.md +2 -10
- package/dist/builtin-memory/05-kinds/explore/00-base.md +2 -2
- package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +0 -2
- package/dist/builtin-memory/05-kinds/general/00-base.md +3 -5
- package/dist/builtin-memory/05-kinds/general/01-orchestrator.md +0 -4
- package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +1 -3
- package/dist/builtin-memory/05-kinds/plan/reviewers/00-base.md +12 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +0 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +0 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +0 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +1 -1
- package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +0 -2
- package/dist/builtin-memory/05-kinds/review/00-base.md +0 -2
- package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -3
- package/dist/builtin-memory/05-kinds/spec/00-base.md +1 -1
- package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +2 -4
- package/dist/builtin-memory/05-kinds/spec/requirements.md +1 -1
- package/dist/builtin-memory/advisor/council.md +40 -0
- package/dist/builtin-memory/design.md +8 -11
- package/dist/builtin-memory/development.md +6 -9
- package/dist/builtin-memory/internal/INDEX.md +5 -5
- package/dist/builtin-memory/internal/agent-shaping.md +5 -7
- package/dist/builtin-memory/internal/examples/imessage-assistant.md +3 -3
- package/dist/builtin-memory/internal/marketplaces.md +13 -14
- package/dist/builtin-memory/internal/memory-loading.md +45 -0
- package/dist/builtin-memory/internal/nodes-and-canvas.md +4 -4
- package/dist/builtin-memory/internal/plugins.md +61 -45
- package/dist/builtin-memory/planning.md +4 -7
- package/dist/builtin-memory/spec.md +9 -12
- package/dist/builtin-memory/wedged-child-on-runaway-bash.md +8 -22
- package/dist/cli.js +1 -1
- package/dist/clients/attach/__tests__/attach-keybindings.test.js +17 -4
- package/dist/clients/attach/__tests__/context-message.test.js +31 -20
- package/dist/clients/attach/__tests__/crtr-output-coverage.test.js +1 -1
- package/dist/clients/attach/__tests__/crtr-output.test.js +39 -30
- package/dist/clients/attach/__tests__/edit-diff.test.js +21 -20
- package/dist/clients/attach/render/chat-view.d.ts +20 -20
- package/dist/clients/attach/render/chat-view.js +88 -89
- package/dist/clients/attach/render/{frozen-history.d.ts → condensed-history.d.ts} +1 -18
- package/dist/clients/attach/render/condensed-history.js +60 -0
- package/dist/clients/attach/render/context-message.js +9 -14
- package/dist/clients/attach/render/crtr-output.d.ts +2 -1
- package/dist/clients/attach/render/crtr-output.js +4 -4
- package/dist/clients/attach/render/edit-diff.d.ts +21 -6
- package/dist/clients/attach/render/edit-diff.js +90 -91
- package/dist/clients/attach/render/tool-calls.d.ts +14 -18
- package/dist/clients/attach/render/tool-calls.js +66 -41
- package/dist/clients/attach/session/bindings.d.ts +5 -3
- package/dist/clients/attach/session/bindings.js +7 -13
- package/dist/clients/attach/session/keys.d.ts +2 -0
- package/dist/clients/attach/session/keys.js +36 -37
- package/dist/clients/attach/session/profile-files.d.ts +8 -0
- package/dist/clients/attach/session/profile-files.js +157 -0
- package/dist/clients/attach/viewer.js +530 -528
- package/dist/commands/memory/lint.js +1 -1
- package/dist/commands/memory/write.js +1 -1
- package/dist/commands/node.js +2 -2
- package/dist/commands/pkg/plugin-inspect.js +6 -7
- package/dist/commands/pkg/plugin-manage.d.ts +1 -1
- package/dist/commands/pkg/plugin-manage.js +131 -19
- package/dist/commands/pkg/plugin.js +2 -2
- package/dist/commands/pkg.js +6 -11
- package/dist/commands/profile/env.js +3 -3
- package/dist/commands/sys/config.js +17 -76
- package/dist/commands/sys/doctor.js +5 -91
- package/dist/commands/sys/setup-wizard.d.ts +9 -11
- package/dist/commands/sys/setup-wizard.js +47 -81
- package/dist/core/__tests__/base-worker-prompt.test.js +18 -21
- package/dist/core/__tests__/command-plugins-surfaces.test.js +39 -5
- package/dist/core/__tests__/command-plugins.test.js +36 -16
- package/dist/core/__tests__/review-model-floor.test.js +2 -2
- package/dist/core/__tests__/tmux-surface.test.js +10 -1
- package/dist/core/command-manifests/manifest.d.ts +24 -0
- package/dist/core/{configured-clis → command-manifests}/manifest.js +21 -25
- package/dist/core/command-manifests/registry.d.ts +1 -2
- package/dist/core/command-manifests/registry.js +2 -2
- package/dist/core/command-manifests/schema.d.ts +11 -13
- package/dist/core/command-manifests/schema.js +53 -192
- package/dist/core/command-plugins/compose.d.ts +0 -6
- package/dist/core/command-plugins/compose.js +28 -75
- package/dist/core/command-plugins/discovery.d.ts +25 -58
- package/dist/core/command-plugins/discovery.js +152 -259
- package/dist/core/command-plugins/endpoint.d.ts +24 -0
- package/dist/core/command-plugins/endpoint.js +48 -0
- package/dist/core/command-plugins/store.d.ts +16 -0
- package/dist/core/command-plugins/store.js +64 -0
- package/dist/core/command-plugins/{adapter.d.ts → transport/exec-invoke.d.ts} +3 -3
- package/dist/core/command-plugins/{adapter.js → transport/exec-invoke.js} +4 -4
- package/dist/core/{configured-clis/fetch.d.ts → command-plugins/transport/http-fetch.d.ts} +9 -18
- package/dist/core/{configured-clis/fetch.js → command-plugins/transport/http-fetch.js} +15 -35
- package/dist/core/{configured-clis/invoker.d.ts → command-plugins/transport/http-invoke.d.ts} +7 -7
- package/dist/core/{configured-clis/invoker.js → command-plugins/transport/http-invoke.js} +21 -23
- package/dist/core/command.d.ts +8 -9
- package/dist/core/command.js +6 -9
- package/dist/core/config.js +6 -10
- package/dist/core/env-name.d.ts +6 -0
- package/dist/core/env-name.js +9 -0
- package/dist/core/io.d.ts +1 -1
- package/dist/core/keybindings/__tests__/resolve.test.js +40 -3
- package/dist/core/keybindings/attach-control.d.ts +37 -0
- package/dist/core/keybindings/attach-control.js +38 -0
- package/dist/core/keybindings/catalog.d.ts +5 -4
- package/dist/core/keybindings/catalog.js +16 -8
- package/dist/core/keybindings/index.d.ts +1 -0
- package/dist/core/keybindings/index.js +1 -0
- package/dist/core/keybindings/types.d.ts +1 -1
- package/dist/core/preview-registry.js +41 -74
- package/dist/core/runtime/bearings.d.ts +2 -5
- package/dist/core/runtime/bearings.js +2 -5
- package/dist/core/runtime/front-door.d.ts +1 -1
- package/dist/core/runtime/front-door.js +2 -2
- package/dist/core/runtime/kickoff.d.ts +3 -3
- package/dist/core/runtime/kickoff.js +4 -3
- package/dist/core/runtime/situational-context.d.ts +1 -1
- package/dist/core/runtime/situational-context.js +1 -1
- package/dist/core/runtime/spawn.js +9 -9
- package/dist/core/runtime/tmux.js +29 -2
- package/dist/core/scope.d.ts +0 -5
- package/dist/core/scope.js +0 -10
- package/dist/core/user-settings.d.ts +193 -0
- package/dist/core/user-settings.js +252 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/pi-extensions/canvas-stophook.js +3 -2
- package/dist/pi-extensions/canvas-tool-guide.js +4 -1
- package/dist/shared/generated-context.d.ts +34 -0
- package/dist/shared/generated-context.js +98 -0
- package/dist/types.d.ts +23 -16
- package/dist/types.js +3 -14
- package/dist/web-client/assets/{index-NIuSCOHM.js → index-BMTOGuOZ.js} +19 -19
- package/dist/web-client/assets/index-DvA6Zw-R.css +2 -0
- package/dist/web-client/index.html +2 -2
- package/dist/web-client/sw.js +1 -1
- package/docs/public-api.md +1 -0
- package/package.json +2 -2
- package/runtime.lock.json +6 -6
- package/dist/builtin-memory/05-kinds/product/00-base.md +0 -25
- package/dist/builtin-memory/05-kinds/product/01-orchestrator.md +0 -15
- package/dist/builtin-memory/05-kinds/product/teardown.md +0 -15
- package/dist/builtin-memory/internal/workflow-codification.md +0 -82
- package/dist/builtin-memory/product.md +0 -80
- package/dist/clients/attach/render/frozen-history.js +0 -100
- package/dist/commands/pkg/cli-inspect.d.ts +0 -17
- package/dist/commands/pkg/cli-inspect.js +0 -190
- package/dist/commands/pkg/cli-manage.d.ts +0 -3
- package/dist/commands/pkg/cli-manage.js +0 -206
- package/dist/commands/pkg/cli.d.ts +0 -1
- package/dist/commands/pkg/cli.js +0 -14
- package/dist/core/configured-clis/cache.d.ts +0 -16
- package/dist/core/configured-clis/cache.js +0 -57
- package/dist/core/configured-clis/compose.d.ts +0 -14
- package/dist/core/configured-clis/compose.js +0 -60
- package/dist/core/configured-clis/discovery.d.ts +0 -47
- package/dist/core/configured-clis/discovery.js +0 -173
- package/dist/core/configured-clis/manifest.d.ts +0 -24
- package/dist/core/configured-clis/registration.d.ts +0 -40
- package/dist/core/configured-clis/registration.js +0 -201
- package/dist/web-client/assets/index-CqLKj8Xu.css +0 -2
|
@@ -70,7 +70,7 @@ export function lintBodyLength(fm, body, docName) {
|
|
|
70
70
|
const sys = fm['system-prompt-visibility'];
|
|
71
71
|
const file = fm['file-read-visibility'];
|
|
72
72
|
const words = countBodyWords(body);
|
|
73
|
-
const remedy = 'Keep the load-bearing core here and split the depth into [[linked]] reference docs saved at `none` visibility on both axes (the link is how they are found, so they cost nothing until followed). Keep it whole — `lint-ignore: length` in the frontmatter — only when every reader who surfaces this doc genuinely benefits from reading 100% of it, or it is one indivisible body of knowledge; then splitting just adds hops.';
|
|
73
|
+
const remedy = 'Keep the load-bearing core here and split the depth into [[linked]] reference docs saved at `none` visibility on both axes (the link is how they are found, so they cost nothing until followed). Split by subject: each leaf covers a different subject a task might need on its own; never split off “further evidence”, examples, or references — a references leaf is never followed, so supporting material stays next to the point it supports or gets cut. Keep it whole — `lint-ignore: length` in the frontmatter — only when every reader who surfaces this doc genuinely benefits from reading 100% of it, or it is one indivisible body of knowledge; then splitting just adds hops.';
|
|
74
74
|
if (sys === 'content' && words > SYSTEM_CONTENT_MAX_WORDS) {
|
|
75
75
|
return `body is ${words} words but system-prompt-visibility: content inlines every word into every agent's system prompt at boot — capped at ${SYSTEM_CONTENT_MAX_WORDS} words (system-prompt preview gets ${SYSTEM_PREVIEW_MAX_WORDS}; name/none are never capped on that axis). ${remedy}`;
|
|
76
76
|
}
|
|
@@ -23,7 +23,7 @@ export const writeLeaf = defineLeaf({
|
|
|
23
23
|
'Write the routing line (--when-and-why-to-read) first, before storing anything: "When <circumstance>, this <kind> should be read because <broader downstream payoff>." WHY is the READER’s payoff — the consequence they secure for the task in front of them by spending the read — never what the document says, its rule, or why it should be obeyed. The trap most authors fall into is the DISGUISED restatement: a because-clause that reads like a benefit but is just the doc’s own thesis reworded as an outcome. It still fails. The test: if you could derive the because-clause by paraphrasing the doc’s advice, it is a restatement, not a payoff — a real payoff names a consequence in the reader’s world that the document itself never asserts. Bad (naked restatement): "because only genuine first principles belong in taste memory." Bad (restatement in benefit’s clothing): "because keeping the test loop fast and free of speculative tests protects the development pace" — that is the doc’s rule in outcome costume, derivable straight from its advice. Good: "because identifying throughlines in user taste lets future decisions be made faster and with less re-litigation." The stranger test: someone mid-task who has NOT read the doc must be able to decide from this one line alone whether the read is worth it. If you cannot name the concrete situation that triggers it, you do not yet understand the memory — ask the user one sharp question instead of improvising.\n\n' +
|
|
24
24
|
'Gate and read-when share the same predicate language: a field map is AND-ed across fields; dotted fields resolve nested values; field matchers may be scalar, array, or object. Scalar matchers do exact equality, with arrays matching any element. Array matchers do membership or intersection. Object matchers accept `eq`, `ne`, `in`, `nin`, `exists`, `contains`, `containsAll`, `containsAny`, `matches`, `imatches`, `gt`, `gte`, `lt`, and `lte`. Combinators are `all`, `any`, and `not`; sibling field matchers next to them are AND-ed in. An empty condition is inert. An unknown op never matches.\n\n' +
|
|
25
25
|
'Link related docs with `[[canonical/name]]`. A doc link in any body is a first-class cross-reference to another memory document by its exact canonical name — the same identifier `crtr memory read` takes (a directory INDEX is linked by its bare directory name). Links are pointers, never transclusion: the linked body is loaded only when a reader follows it, so a link costs nothing until needed. `crtr memory lint` fails on a link that resolves to no document, and `crtr memory read` lists a doc’s resolvable links alongside its body. There is no alias or label form.\n\n' +
|
|
26
|
-
'When a doc grows long or information-rich, nest it into a graph instead of letting it become a scroll. The main doc at the topic’s path keeps the high-level, most load-bearing information, most important first; depth splits into reference docs under the topic’s directory (`area/topic/...`), each pointed at with a `[[link]]
|
|
26
|
+
'When a doc grows long or information-rich, nest it into a graph instead of letting it become a scroll. The main doc at the topic’s path keeps the high-level, most load-bearing information, most important first; depth splits into reference docs under the topic’s directory (`area/topic/...`), each pointed at with a `[[link]]`. Split by subject: a leaf earns its link by covering a different subject a task might need on its own; a leaf of offloaded “further evidence”, examples, or references is never followed, so supporting material either sits in the main doc next to the point it supports or gets cut. The main doc is the entry point a reader can act from alone; a reference leaf is loaded only when the task needs that depth. Save reference leaves at `none` visibility on both axes — the link from the main doc is how they are found, so any higher rung just double-charges every boot or read for depth the graph already routes. This is the same layering the visibility rungs apply to boot cost, applied to the body itself. Keep every doc as short as its job allows; `crtr memory lint` caps body length by rung and its findings carry the split guidance.\n\n' +
|
|
27
27
|
'Find before write. Prefer slightly expanding an existing document, nesting genuinely separate depth under its topic, and updating the existing `when-and-why-to-read` (plus its INDEX router when present) over creating another similar memory. A new document earns its own identity only when it has a distinct read trigger and a coherent body that would make the existing document harder to route or use. Group related docs with path names (area/topic). Do not store what is already recorded or what only matters to this conversation. Body is for current truth, not history. Provenance is automatic on create and preserved on update. Run `crtr memory lint` — the frontmatter validator — after authoring.\n\n' +
|
|
28
28
|
'--rationale is the gap this doc exists to close — the observed agent failure that prompted it, captured from user signal (a correction, a mistake you watched happen) rather than inferred from the doc’s own content. The bar: if the rationale is guessable from reading the doc, it is not the real one — a guessable gap is one agents do not actually fall into, so a merely-plausible-sounding reason is not worth recording. It is maintainer-facing only and NEVER ships in a delivered surface (boot render, on-read injection, `memory read` content) — it lives in frontmatter, visible only via `memory read --frontmatter`. Omit it when you have no such gap to record; omitting the flag on an update always preserves whatever rationale already exists.',
|
|
29
29
|
params: [
|
package/dist/commands/node.js
CHANGED
|
@@ -247,7 +247,7 @@ const nodeCreationEngine = defineLeaf({
|
|
|
247
247
|
'Creates nodes/<id>/ in the canvas home, indexes the node in canvas.db, and starts its detached broker engine.',
|
|
248
248
|
'A managed child creates an active parent subscription; --root creates an independent node with spawned_by provenance and no parent subscription.',
|
|
249
249
|
'--worktree creates a managed worktree. --output-schema writes nodes/<id>/output-schema.json. --no-kickoff starts an idle root without a first model turn.',
|
|
250
|
-
'An in-tmux root opens a viewer
|
|
250
|
+
'An in-tmux root opens a viewer. When auto_open_child_viewers is true, a managed child opens beside a live spawner viewer; false leaves it running in the background.',
|
|
251
251
|
],
|
|
252
252
|
},
|
|
253
253
|
run: async (input) => {
|
|
@@ -384,7 +384,7 @@ const nodeNew = defineLeaf({
|
|
|
384
384
|
'Creates nodes/<id>/ in the canvas home, indexes the node in canvas.db, and starts its detached broker engine.',
|
|
385
385
|
'A managed child creates an active parent subscription; its pushes wake the parent. --root creates an independent node with spawned_by provenance and no parent subscription.',
|
|
386
386
|
'--worktree creates a managed worktree at ~/.crouter/canvas/worktrees/<node-id>/ on a new crtr/<node-id> branch. --output-schema writes nodes/<id>/output-schema.json. --no-kickoff starts an idle root without a first model turn.',
|
|
387
|
-
'An in-tmux root opens a viewer in the current tmux session
|
|
387
|
+
'An in-tmux root opens a viewer in the current tmux session. When auto_open_child_viewers is true, a managed child opens beside a live spawner viewer; false leaves it running in the background.'
|
|
388
388
|
],
|
|
389
389
|
},
|
|
390
390
|
run: async (input, context) => {
|
|
@@ -2,7 +2,7 @@ import { defineLeaf } from '../../core/command.js';
|
|
|
2
2
|
import { notFound } from '../../core/errors.js';
|
|
3
3
|
import { paginate } from '../../core/pagination.js';
|
|
4
4
|
import { listInstalledPlugins, findPluginByName, } from '../../core/resolver.js';
|
|
5
|
-
import {
|
|
5
|
+
import { listPluginMemoryDocs } from '../../core/memory-resolver.js';
|
|
6
6
|
import { validatePluginCommands, validateEffectiveCommandPlugins } from '../../core/command-plugins/discovery.js';
|
|
7
7
|
import { SUBTREE_NAMES } from '../../build-root.js';
|
|
8
8
|
import { resolveScopeArg, projectScopeRoot } from '../../core/scope.js';
|
|
@@ -20,7 +20,7 @@ export const pluginList = defineLeaf({
|
|
|
20
20
|
{ kind: 'flag', name: 'cursor', type: 'string', required: false, constraint: 'Opaque token from next_cursor. Omit on first call.' },
|
|
21
21
|
],
|
|
22
22
|
output: [
|
|
23
|
-
{ name: 'items', type: 'object[]', required: true, constraint: 'Each: {name, scope, version?, enabled, source_marketplace?, path}. Sorted by scope then name ascending.' },
|
|
23
|
+
{ name: 'items', type: 'object[]', required: true, constraint: 'Each: {name, scope, version?, enabled, transport, source_marketplace?, path}. transport is exec or http for command plugins, null for docs-only plugins. Sorted by scope then name ascending.' },
|
|
24
24
|
{ name: 'next_cursor', type: 'string | null', required: true, constraint: 'null means no more items.' },
|
|
25
25
|
{ name: 'total', type: 'integer | null', required: true, constraint: 'Exact when cheap; null otherwise.' },
|
|
26
26
|
],
|
|
@@ -65,6 +65,7 @@ export const pluginList = defineLeaf({
|
|
|
65
65
|
scope: p.scope,
|
|
66
66
|
version: p.version,
|
|
67
67
|
enabled: p.enabled,
|
|
68
|
+
transport: p.manifest.transport?.kind ?? null,
|
|
68
69
|
source_marketplace: p.sourceMarketplace,
|
|
69
70
|
path: p.root,
|
|
70
71
|
})),
|
|
@@ -91,10 +92,10 @@ export const pluginShow = defineLeaf({
|
|
|
91
92
|
{ name: 'enabled', type: 'boolean', required: true, constraint: 'Whether the plugin is active.' },
|
|
92
93
|
{ name: 'manifest', type: 'object', required: true, constraint: 'Full plugin.json contents.' },
|
|
93
94
|
{ name: 'docs', type: 'object[]', required: true, constraint: 'Each: {name, path}. Memory docs provided by the plugin (its `<pluginName>/` memory subtree).' },
|
|
94
|
-
{ name: 'commands', type: 'object', required: false, constraint: 'Present only when the plugin declares a command manifest (manifest.commands). {manifestPath: string,
|
|
95
|
+
{ name: 'commands', type: 'object', required: false, constraint: 'Present only when the plugin declares a command manifest (manifest.commands). {manifestPath: string, mounts: string[] (accepted top-level command names), issues: object[] ({code, path?, message, received, expected, next})}. Validated statically; command execution never occurs.' },
|
|
95
96
|
],
|
|
96
97
|
outputKind: 'object',
|
|
97
|
-
effects: ['None. Read-only. Command manifests are validated statically;
|
|
98
|
+
effects: ['None. Read-only. Command manifests are validated statically; commands are never executed.'],
|
|
98
99
|
},
|
|
99
100
|
run: async (input) => {
|
|
100
101
|
const name = input['name'];
|
|
@@ -115,8 +116,7 @@ export const pluginShow = defineLeaf({
|
|
|
115
116
|
if (found === null) {
|
|
116
117
|
throw notFound(`plugin not found: ${name}`);
|
|
117
118
|
}
|
|
118
|
-
const
|
|
119
|
-
const docs = listAllMemoryDocs().filter((d) => d.name.startsWith(prefix));
|
|
119
|
+
const docs = listPluginMemoryDocs(found, found.scope);
|
|
120
120
|
let commands;
|
|
121
121
|
if (typeof found.manifest.commands === 'string' && found.manifest.commands.length > 0) {
|
|
122
122
|
const reserved = new Set(SUBTREE_NAMES);
|
|
@@ -129,7 +129,6 @@ export const pluginShow = defineLeaf({
|
|
|
129
129
|
validatePluginCommands(found, reserved);
|
|
130
130
|
commands = {
|
|
131
131
|
manifestPath: v.manifestPath,
|
|
132
|
-
executable: v.executable ?? null,
|
|
133
132
|
mounts: v.contributions.map((c) => c.node.name),
|
|
134
133
|
issues: v.issues,
|
|
135
134
|
};
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { join, resolve, isAbsolute } from 'node:path';
|
|
2
2
|
import { renameSync } from 'node:fs';
|
|
3
3
|
import { defineLeaf } from '../../core/command.js';
|
|
4
|
-
import { notFound, usage, general } from '../../core/errors.js';
|
|
4
|
+
import { CrtrError, notFound, usage, general, network } from '../../core/errors.js';
|
|
5
5
|
import { findMarketplaceByName, findPluginByName, listAllPlugins } from '../../core/resolver.js';
|
|
6
6
|
import { pluginsDir, ensureProjectScopeRoot, userScopeRoot, resolveScopeArg, projectScopeRoot, requireScopeRoot, } from '../../core/scope.js';
|
|
7
7
|
import { updateConfig, updateState, ensureScopeInitialized } from '../../core/config.js';
|
|
8
|
-
import { pathExists, ensureDir, removePath, nowIso, linkOrCopy, isSymlink } from '../../core/fs-utils.js';
|
|
8
|
+
import { pathExists, ensureDir, removePath, nowIso, linkOrCopy, isSymlink, atomicWriteJson } from '../../core/fs-utils.js';
|
|
9
9
|
import { clone, pull, deriveNameFromUrl, currentSha, isGitRepo } from '../../core/git.js';
|
|
10
|
-
import { readMarketplaceManifest, readPluginManifest } from '../../core/manifest.js';
|
|
10
|
+
import { pluginManifestPath, readMarketplaceManifest, readPluginManifest } from '../../core/manifest.js';
|
|
11
11
|
import { validatePluginCommands, validateEffectiveCommandPlugins } from '../../core/command-plugins/discovery.js';
|
|
12
|
+
import { normalizeEndpoint, validateEndpoint, validateHttpPluginTransport } from '../../core/command-plugins/endpoint.js';
|
|
13
|
+
import { resolveHttpPluginManifestStore, writeHttpPluginManifest } from '../../core/command-plugins/store.js';
|
|
14
|
+
import { fetchHttpPluginManifest, refreshHttpPlugin } from '../../core/command-plugins/transport/http-fetch.js';
|
|
12
15
|
import { SUBTREE_NAMES } from '../../build-root.js';
|
|
16
|
+
import { ExitCode } from '../../types.js';
|
|
13
17
|
import { isGitUrl, setPluginEnabled, resolveInstallScope } from './shared.js';
|
|
14
18
|
/** Validate a just-installed/updated plugin's command manifest, if it declares
|
|
15
19
|
* one. Returns undefined for plugins without a `commands` pointer. Never
|
|
@@ -27,6 +31,68 @@ function commandReport(name, scope) {
|
|
|
27
31
|
validatePluginCommands(plugin, reserved);
|
|
28
32
|
return { mounts: v.contributions.map((c) => c.node.name), issues: v.issues };
|
|
29
33
|
}
|
|
34
|
+
const PLUGIN_NAME_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
35
|
+
function unsafeHttpManifestPath(name, commands) {
|
|
36
|
+
throw new CrtrError('command_path_unsafe', `HTTP plugin "${name}" commands pointer escapes the plugin root or is not a regular file`, ExitCode.USAGE, { received: commands, field: 'commands', next: 'Fix plugin.json.commands.' });
|
|
37
|
+
}
|
|
38
|
+
function validateHttpInstall(name, endpoint, authEnv) {
|
|
39
|
+
if (typeof name !== 'string' || !PLUGIN_NAME_RE.test(name)) {
|
|
40
|
+
throw usage('invalid --name: expected a lowercase kebab-case plugin handle');
|
|
41
|
+
}
|
|
42
|
+
const endpointValidation = validateEndpoint(endpoint);
|
|
43
|
+
if (!endpointValidation.isValid) {
|
|
44
|
+
throw usage(`invalid --endpoint: ${endpointValidation.errors.join('; ')}`);
|
|
45
|
+
}
|
|
46
|
+
const normalizedEndpoint = normalizeEndpoint(endpoint);
|
|
47
|
+
const transportValidation = validateHttpPluginTransport({
|
|
48
|
+
kind: 'http',
|
|
49
|
+
endpoint: normalizedEndpoint,
|
|
50
|
+
...(authEnv !== undefined ? { authEnv } : {}),
|
|
51
|
+
});
|
|
52
|
+
if (transportValidation.transport === undefined) {
|
|
53
|
+
throw usage(`invalid HTTP plugin transport: ${transportValidation.errors.join('; ')}`);
|
|
54
|
+
}
|
|
55
|
+
return transportValidation.transport;
|
|
56
|
+
}
|
|
57
|
+
async function installHttpPlugin(name, endpoint, authEnv, scope) {
|
|
58
|
+
const transport = validateHttpInstall(name, endpoint, authEnv);
|
|
59
|
+
const scopeRootPath = scope === 'project' ? ensureProjectScopeRoot() : userScopeRoot();
|
|
60
|
+
const root = join(scopeRootPath, 'plugins', name);
|
|
61
|
+
if (pathExists(root) && readPluginManifest(root)?.transport?.kind !== 'http') {
|
|
62
|
+
throw general(`plugin "${name}" already exists at ${root} and is not an HTTP-transport plugin`);
|
|
63
|
+
}
|
|
64
|
+
if (pathExists(root) && resolveHttpPluginManifestStore({ root, commands: 'commands.json' }) === null) {
|
|
65
|
+
unsafeHttpManifestPath(name, 'commands.json');
|
|
66
|
+
}
|
|
67
|
+
const fetched = await fetchHttpPluginManifest({
|
|
68
|
+
name,
|
|
69
|
+
endpoint: transport.endpoint,
|
|
70
|
+
...(transport.authEnv !== undefined ? { authEnv: transport.authEnv } : {}),
|
|
71
|
+
});
|
|
72
|
+
if (fetched.status !== 200) {
|
|
73
|
+
const message = `HTTP plugin install failed for "${name}": ${fetched.message}`;
|
|
74
|
+
if (fetched.status === 'auth_env_missing')
|
|
75
|
+
throw usage(message);
|
|
76
|
+
if (fetched.status === 'cli_unreachable')
|
|
77
|
+
throw network(message);
|
|
78
|
+
throw general(message);
|
|
79
|
+
}
|
|
80
|
+
ensureScopeInitialized(scope, scopeRootPath);
|
|
81
|
+
atomicWriteJson(pluginManifestPath(root), {
|
|
82
|
+
name,
|
|
83
|
+
commands: 'commands.json',
|
|
84
|
+
transport,
|
|
85
|
+
});
|
|
86
|
+
const store = resolveHttpPluginManifestStore({ root, commands: 'commands.json' });
|
|
87
|
+
if (store === null)
|
|
88
|
+
unsafeHttpManifestPath(name, 'commands.json');
|
|
89
|
+
writeHttpPluginManifest(store, fetched.raw);
|
|
90
|
+
updateConfig(scope, (cfg) => {
|
|
91
|
+
cfg.plugins[name] = { enabled: true };
|
|
92
|
+
});
|
|
93
|
+
const commands = commandReport(name, scope);
|
|
94
|
+
return { name, scope, path: root, transport: 'http', ...(commands !== undefined ? { commands } : {}) };
|
|
95
|
+
}
|
|
30
96
|
function classifyInstallRef(raw) {
|
|
31
97
|
if (raw.startsWith('./') || raw.startsWith('../') || raw.startsWith('/')) {
|
|
32
98
|
return { kind: 'local', value: raw };
|
|
@@ -202,7 +268,23 @@ export function installFromMarketplace(ref, scope) {
|
|
|
202
268
|
});
|
|
203
269
|
return { name: ref.plugin, scope, path: destPluginDir };
|
|
204
270
|
}
|
|
205
|
-
function updateOnePlugin(plugin, marketplaceCache, opts) {
|
|
271
|
+
async function updateOnePlugin(plugin, marketplaceCache, opts) {
|
|
272
|
+
if (plugin.manifest.transport?.kind === 'http') {
|
|
273
|
+
const commandsPath = plugin.manifest.commands;
|
|
274
|
+
if (typeof commandsPath !== 'string' || commandsPath.length === 0) {
|
|
275
|
+
throw usage(`HTTP plugin "${plugin.name}" has no commands path in plugin.json; fix the manifest before updating.`);
|
|
276
|
+
}
|
|
277
|
+
const store = resolveHttpPluginManifestStore({ root: plugin.root, commands: commandsPath });
|
|
278
|
+
if (store === null)
|
|
279
|
+
unsafeHttpManifestPath(plugin.name, commandsPath);
|
|
280
|
+
await refreshHttpPlugin({
|
|
281
|
+
name: plugin.name,
|
|
282
|
+
endpoint: plugin.manifest.transport.endpoint,
|
|
283
|
+
...(plugin.manifest.transport.authEnv !== undefined ? { authEnv: plugin.manifest.transport.authEnv } : {}),
|
|
284
|
+
}, store);
|
|
285
|
+
const commands = commandReport(plugin.name, plugin.scope);
|
|
286
|
+
return { name: plugin.name, transport: 'http', updated: true, ...(commands !== undefined ? { commands } : {}) };
|
|
287
|
+
}
|
|
206
288
|
let contentUpdate = { updated: false, sha: currentSha(plugin.root) ?? '' };
|
|
207
289
|
let marketplaceVersion;
|
|
208
290
|
if (plugin.sourceMarketplace !== undefined) {
|
|
@@ -227,6 +309,7 @@ function updateOnePlugin(plugin, marketplaceCache, opts) {
|
|
|
227
309
|
const commands = commandReport(plugin.name, plugin.scope);
|
|
228
310
|
return {
|
|
229
311
|
name: plugin.name,
|
|
312
|
+
...(plugin.manifest.transport?.kind === 'exec' ? { transport: 'exec' } : {}),
|
|
230
313
|
updated: contentUpdate.updated,
|
|
231
314
|
sha: contentUpdate.sha,
|
|
232
315
|
...(commands !== undefined ? { commands } : {}),
|
|
@@ -234,39 +317,66 @@ function updateOnePlugin(plugin, marketplaceCache, opts) {
|
|
|
234
317
|
}
|
|
235
318
|
export const pluginInstall = defineLeaf({
|
|
236
319
|
name: 'install',
|
|
237
|
-
description: 'install from
|
|
238
|
-
whenToUse: 'installing one plugin
|
|
320
|
+
description: 'install from a ref or HTTP endpoint',
|
|
321
|
+
whenToUse: 'installing one plugin from a marketplace ref, git URL, local path, or an HTTP command-manifest endpoint',
|
|
239
322
|
help: {
|
|
240
323
|
name: 'pkg plugin install',
|
|
241
|
-
summary: 'install a plugin from
|
|
324
|
+
summary: 'install a plugin from a ref or HTTP endpoint',
|
|
242
325
|
params: [
|
|
243
|
-
{ kind: 'positional', name: 'install-ref', type: 'string', required:
|
|
326
|
+
{ kind: 'positional', name: 'install-ref', type: 'string', required: false, constraint: 'Plugin install ref. Provide exactly one of INSTALL-REF or --endpoint. Accepted ref forms: marketplace/plugin; git URL starting https://, ssh://, git@, file://, or ending .git; local path starting ./, ../, or /.' },
|
|
327
|
+
{ kind: 'flag', name: 'endpoint', type: 'string', required: false, constraint: 'HTTP plugin endpoint. Provide exactly one of --endpoint or INSTALL-REF. Must be an absolute https: URL; http: is permitted only for localhost, 127.0.0.1, [::1], ::1, or host.docker.internal. Credentials and fragments are rejected.' },
|
|
328
|
+
{ kind: 'flag', name: 'name', type: 'string', required: false, constraint: 'Required with --endpoint and rejected without it. Lowercase kebab-case plugin handle.' },
|
|
329
|
+
{ kind: 'flag', name: 'auth-env', type: 'string', required: false, constraint: 'Optional environment variable holding the HTTP bearer token. Accepted only with --endpoint; its value is never stored.' },
|
|
244
330
|
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Default: project if available, else user.' },
|
|
245
|
-
{ kind: 'flag', name: 'ref', type: 'string', required: false, constraint: 'Git ref (branch/tag) for git URL installs.
|
|
331
|
+
{ kind: 'flag', name: 'ref', type: 'string', required: false, constraint: 'Git ref (branch/tag) for git URL installs. Rejected with --endpoint; ignored for marketplace and local path refs.' },
|
|
246
332
|
],
|
|
247
333
|
output: [
|
|
248
334
|
{ name: 'name', type: 'string', required: true, constraint: 'Plugin name as installed.' },
|
|
249
335
|
{ name: 'scope', type: 'string', required: true, constraint: 'Scope the plugin was installed into.' },
|
|
250
336
|
{ name: 'path', type: 'string', required: true, constraint: 'Absolute path to the installed plugin directory.' },
|
|
251
|
-
{ name: '
|
|
337
|
+
{ name: 'transport', type: 'string', required: false, constraint: 'exec or http when the plugin declares commands. Always http for an --endpoint install.' },
|
|
338
|
+
{ name: 'commands', type: 'object', required: false, constraint: 'Present only when the plugin declares a command manifest. {mounts: string[] (accepted top-level command names now live for the next invocation), issues: object[] (typed validation issues that rejected a contribution — {code, path?, message, received, expected, next})}. Validated statically; commands are never executed.' },
|
|
252
339
|
],
|
|
253
340
|
outputKind: 'object',
|
|
254
341
|
effects: [
|
|
255
|
-
'
|
|
256
|
-
'
|
|
342
|
+
'A ref install clones, links, or copies the plugin into the scope plugins directory and registers it with enabled=true. Marketplace installs refresh the source marketplace before resolving the plugin entry.',
|
|
343
|
+
'An --endpoint install validates inputs, fetches the command manifest before writing, then writes plugin.json and commands.json and enables the plugin. Reinstalling the same HTTP plugin name replaces its stored manifest and metadata; a fetch failure writes nothing.',
|
|
344
|
+
'If the plugin declares a command manifest, its commands go live on the next crtr invocation. Exec commands run trusted local code only when explicitly invoked; HTTP commands call their declared endpoint only when explicitly invoked.',
|
|
257
345
|
],
|
|
258
346
|
},
|
|
259
347
|
run: async (input) => {
|
|
260
348
|
const rawRef = input['installRef'];
|
|
349
|
+
const endpoint = input['endpoint'];
|
|
350
|
+
const name = input['name'];
|
|
351
|
+
const authEnv = input['authEnv'];
|
|
261
352
|
const scopeInput = input['scope'];
|
|
262
353
|
const gitRef = input['ref'];
|
|
263
354
|
const scope = resolveInstallScope(scopeInput);
|
|
355
|
+
if ((rawRef === undefined) === (endpoint === undefined)) {
|
|
356
|
+
throw usage('provide exactly one of INSTALL-REF or --endpoint');
|
|
357
|
+
}
|
|
358
|
+
if (endpoint !== undefined) {
|
|
359
|
+
if (name === undefined)
|
|
360
|
+
throw usage('--name is required with --endpoint');
|
|
361
|
+
if (gitRef !== undefined)
|
|
362
|
+
throw usage('--ref is only valid with a git INSTALL-REF, not --endpoint');
|
|
363
|
+
return installHttpPlugin(name, endpoint, authEnv, scope);
|
|
364
|
+
}
|
|
365
|
+
if (name !== undefined)
|
|
366
|
+
throw usage('--name is only valid with --endpoint');
|
|
367
|
+
if (authEnv !== undefined)
|
|
368
|
+
throw usage('--auth-env is only valid with --endpoint');
|
|
264
369
|
const classified = classifyInstallRef(rawRef);
|
|
265
370
|
const installed = classified.kind === 'marketplace' ? installFromMarketplace(classified.value, scope)
|
|
266
371
|
: classified.kind === 'local' ? installFromLocal(classified.value, scope)
|
|
267
372
|
: installFromGit(classified.value, gitRef, scope);
|
|
373
|
+
const plugin = findPluginByName(installed.name, installed.scope);
|
|
268
374
|
const commands = commandReport(installed.name, installed.scope);
|
|
269
|
-
return
|
|
375
|
+
return {
|
|
376
|
+
...installed,
|
|
377
|
+
...(plugin?.manifest.transport !== undefined ? { transport: plugin.manifest.transport.kind } : {}),
|
|
378
|
+
...(commands !== undefined ? { commands } : {}),
|
|
379
|
+
};
|
|
270
380
|
},
|
|
271
381
|
});
|
|
272
382
|
export const pluginRemove = defineLeaf({
|
|
@@ -368,18 +478,18 @@ export const pluginDisable = defineLeaf({
|
|
|
368
478
|
export const pluginUpdate = defineLeaf({
|
|
369
479
|
name: 'update',
|
|
370
480
|
description: 'update installed plugin content',
|
|
371
|
-
whenToUse: '
|
|
481
|
+
whenToUse: 'refreshing one named plugin or all installed plugins: HTTP plugins re-fetch their command manifest, while local plugins retain their git update behavior',
|
|
372
482
|
help: {
|
|
373
483
|
name: 'pkg plugin update',
|
|
374
|
-
summary: '
|
|
484
|
+
summary: 'refresh one or all installed plugins',
|
|
375
485
|
params: [
|
|
376
486
|
{ kind: 'flag', name: 'name', type: 'string', required: false, constraint: 'Plugin name to update. Omit to update all installed plugins.' },
|
|
377
487
|
],
|
|
378
488
|
output: [
|
|
379
|
-
{ name: 'updated', type: 'object[]', required: true, constraint: 'One entry per plugin processed: {name, updated, sha
|
|
489
|
+
{ name: 'updated', type: 'object[]', required: true, constraint: 'One entry per plugin processed: {name, transport?, updated, sha?, commands?}. HTTP plugins report transport=http and replace commands.json after an authenticated refetch; sha is present for git updates. commands is present only for plugins declaring a command manifest: {mounts: string[] (accepted top-level command names), issues: object[] ({code, path?, message, received, expected, next})} — validated statically, never executing commands.' },
|
|
380
490
|
],
|
|
381
491
|
outputKind: 'object',
|
|
382
|
-
effects: ['
|
|
492
|
+
effects: ['HTTP plugins unconditionally re-fetch and replace commands.json; a fetch failure exits nonzero and preserves prior bytes. Other plugins retain their git pull, version, and last_updated behavior.'],
|
|
383
493
|
},
|
|
384
494
|
run: async (input) => {
|
|
385
495
|
const name = input['name'];
|
|
@@ -389,10 +499,12 @@ export const pluginUpdate = defineLeaf({
|
|
|
389
499
|
if (found === null) {
|
|
390
500
|
throw notFound(`plugin not found: ${name}`);
|
|
391
501
|
}
|
|
392
|
-
return { updated: [updateOnePlugin(found, marketplaceCache, { strict: true })] };
|
|
502
|
+
return { updated: [await updateOnePlugin(found, marketplaceCache, { strict: true })] };
|
|
393
503
|
}
|
|
394
504
|
const targets = listAllPlugins().filter((p) => p.scope !== 'builtin');
|
|
395
|
-
const results =
|
|
505
|
+
const results = [];
|
|
506
|
+
for (const plugin of targets)
|
|
507
|
+
results.push(await updateOnePlugin(plugin, marketplaceCache, { strict: false }));
|
|
396
508
|
return { updated: results };
|
|
397
509
|
},
|
|
398
510
|
});
|
|
@@ -4,11 +4,11 @@ import { pluginList, pluginShow } from './plugin-inspect.js';
|
|
|
4
4
|
export const pluginBranch = defineBranch({
|
|
5
5
|
name: 'plugin',
|
|
6
6
|
description: 'install and manage plugins',
|
|
7
|
-
whenToUse: 'working with individual plugins: install
|
|
7
|
+
whenToUse: 'working with individual plugins: install, remove, enable, disable, update, list, or inspect them. Use `pkg market` only for marketplace source registration, refresh, and browsing.',
|
|
8
8
|
help: {
|
|
9
9
|
name: 'pkg plugin',
|
|
10
10
|
summary: 'plugin lifecycle and inspection',
|
|
11
|
-
model: '
|
|
11
|
+
model: 'A plugin may contribute memory docs and commands. Command plugins declare one transport in plugin.json: exec runs a local executable; http calls a remote endpoint. Install accepts either a source ref or an HTTP endpoint.',
|
|
12
12
|
},
|
|
13
13
|
children: [pluginInstall, pluginRemove, pluginEnable, pluginDisable, pluginUpdate, pluginList, pluginShow],
|
|
14
14
|
});
|
package/dist/commands/pkg.js
CHANGED
|
@@ -1,24 +1,19 @@
|
|
|
1
|
-
// `crtr pkg` subtree: plugin lifecycle
|
|
2
|
-
// configured-CLI lifecycle.
|
|
3
|
-
// Sub-branches: plugin {install,remove,enable,disable,update,list,show}
|
|
4
|
-
// market {add,remove,update,list,browse}
|
|
5
|
-
// cli {register,remove,list,show,refresh}
|
|
1
|
+
// `crtr pkg` subtree: plugin lifecycle and marketplace source management.
|
|
6
2
|
import { defineBranch } from '../core/command.js';
|
|
7
3
|
import { pluginBranch } from './pkg/plugin.js';
|
|
8
4
|
import { marketBranch } from './pkg/market.js';
|
|
9
|
-
import { cliBranch } from './pkg/cli.js';
|
|
10
5
|
export function registerPkg() {
|
|
11
6
|
return defineBranch({
|
|
12
7
|
name: 'pkg',
|
|
13
8
|
rootEntry: {
|
|
14
|
-
concept: 'plugins
|
|
15
|
-
desc: 'manage plugins
|
|
16
|
-
useWhen: 'installing or browsing
|
|
9
|
+
concept: 'plugins and marketplaces that extend crtr',
|
|
10
|
+
desc: 'manage plugins and marketplace sources',
|
|
11
|
+
useWhen: 'installing or browsing plugins and marketplace sources; plugin commands may use local executable or remote HTTP transport',
|
|
17
12
|
},
|
|
18
13
|
help: {
|
|
19
14
|
name: 'pkg',
|
|
20
|
-
summary: 'manage plugins
|
|
15
|
+
summary: 'manage plugins and marketplace sources',
|
|
21
16
|
},
|
|
22
|
-
children: [pluginBranch, marketBranch
|
|
17
|
+
children: [pluginBranch, marketBranch],
|
|
23
18
|
});
|
|
24
19
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { defineLeaf, defineBranch } from '../../core/command.js';
|
|
10
10
|
import { InputError } from '../../core/io.js';
|
|
11
11
|
import { loadProfileManifest } from '../../core/profiles/manifest.js';
|
|
12
|
-
import { validateEnvVarName } from '../../core/
|
|
12
|
+
import { validateEnvVarName } from '../../core/env-name.js';
|
|
13
13
|
import { listProfileEnvNames, setProfileEnvVar, removeProfileEnvVar } from '../../core/profiles/env-store.js';
|
|
14
14
|
/** Strips exactly one trailing line ending (`\r\n` or `\n`) — the one
|
|
15
15
|
* documented byte the piped-value contract trims — and leaves every other
|
|
@@ -38,7 +38,7 @@ function requireValidName(name) {
|
|
|
38
38
|
const setLeaf = defineLeaf({
|
|
39
39
|
name: 'set',
|
|
40
40
|
description: 'store one env var for a profile, value piped on stdin',
|
|
41
|
-
whenToUse: 'a profile needs a credential or config value injected directly into every broker launched under it \u2014 e.g. a
|
|
41
|
+
whenToUse: 'a profile needs a credential or config value injected directly into every broker launched under it \u2014 e.g. a HTTP-transport plugin\u2019s `auth-env` token. Setting it here crosses the broker spawn boundary with no separate `spawnEnv.allow` entry required; re-running with the same name overwrites the value.',
|
|
42
42
|
help: {
|
|
43
43
|
name: 'profile env set',
|
|
44
44
|
summary: 'write (or overwrite) one env var in a profile\u2019s 0600 env store',
|
|
@@ -184,7 +184,7 @@ const removeLeaf = defineLeaf({
|
|
|
184
184
|
export const envBranch = defineBranch({
|
|
185
185
|
name: 'env',
|
|
186
186
|
description: 'manage a profile-scoped store of env vars injected into its brokers',
|
|
187
|
-
whenToUse: 'a profile needs a credential or config value available directly to every broker (and thus every node) launched under it \u2014 e.g. a
|
|
187
|
+
whenToUse: 'a profile needs a credential or config value available directly to every broker (and thus every node) launched under it \u2014 e.g. a HTTP-transport plugin\u2019s `auth-env` token \u2014 without exporting it into the daemon\u2019s own ambient shell or granting a `spawnEnv.allow` passthrough.',
|
|
188
188
|
help: {
|
|
189
189
|
name: 'profile env',
|
|
190
190
|
summary: 'a profile\u2019s own 0600 store of env vars, injected directly into every broker launched under it',
|
|
@@ -3,22 +3,12 @@ import { readConfig, updateConfig, persistModelLadders, configPath as coreConfig
|
|
|
3
3
|
import { usage, notFound } from '../../core/errors.js';
|
|
4
4
|
import { scopeRoot, requireScopeRoot, listScopes } from '../../core/scope.js';
|
|
5
5
|
import { resolveScope } from './shared.js';
|
|
6
|
-
import {
|
|
6
|
+
import { SCOPE_CONFIG_KEYS, USER_SETTINGS, findUserSetting } from '../../core/user-settings.js';
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
8
|
// Config helpers (ported from commands/config.ts)
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
'marketplaces',
|
|
13
|
-
'plugins',
|
|
14
|
-
'max_panes_per_window',
|
|
15
|
-
'brokerThresholds',
|
|
16
|
-
'completion_bell',
|
|
17
|
-
'live_cycles',
|
|
18
|
-
'condensed_history',
|
|
19
|
-
'fold_finished_tools',
|
|
20
|
-
'modelLadders',
|
|
21
|
-
]);
|
|
10
|
+
const CONFIG_TOP_LEVEL_HELP = SCOPE_CONFIG_KEYS.join(', ');
|
|
11
|
+
const SCALAR_SETTING_HELP = USER_SETTINGS.map((setting) => `${setting.key} (${setting.valueConstraint})`).join(', ');
|
|
22
12
|
const MODEL_PROVIDERS = new Set(['anthropic', 'openai']);
|
|
23
13
|
const MODEL_STRENGTHS = new Set(['ultra', 'strong', 'medium', 'light']);
|
|
24
14
|
function isModelProvider(value) {
|
|
@@ -51,6 +41,11 @@ function assertUserScopeBrokerThresholds(key, scope) {
|
|
|
51
41
|
throw usage('brokerThresholds are available only in user scope');
|
|
52
42
|
}
|
|
53
43
|
}
|
|
44
|
+
function assertUserSettingScope(key, scope) {
|
|
45
|
+
if (findUserSetting(key) !== undefined && scope !== 'user') {
|
|
46
|
+
throw usage(`${key} is available only in user scope`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
54
49
|
/** Validate a `modelLadders.*` key/value into an explicit sparse patch, or
|
|
55
50
|
* return null when the key is not a ladder key. Ladder sets do NOT go through
|
|
56
51
|
* `setNestedValue`/`updateConfig`: that path persists only what differs from
|
|
@@ -91,29 +86,11 @@ function setNestedValue(cfg, key, rawValue, value) {
|
|
|
91
86
|
if (topKey === 'modelLadders') {
|
|
92
87
|
throw usage('modelLadders keys are written through persistModelLadders, not this path');
|
|
93
88
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
throw usage(`auto_update.content must be 'notify', 'apply', or false`);
|
|
100
|
-
}
|
|
101
|
-
cfg.auto_update.content = value;
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
if (key === 'auto_update.crtr') {
|
|
105
|
-
const coerced = value === true ? 'notify' : value;
|
|
106
|
-
if (coerced !== 'notify' && coerced !== 'apply' && coerced !== false) {
|
|
107
|
-
throw usage(`auto_update.crtr must be 'notify', 'apply', or false`);
|
|
108
|
-
}
|
|
109
|
-
cfg.auto_update.crtr = coerced;
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
if (key === 'max_panes_per_window') {
|
|
113
|
-
if (typeof value !== 'number' || !Number.isFinite(value) || value < 1) {
|
|
114
|
-
throw usage(`max_panes_per_window must be an integer >= 1`);
|
|
115
|
-
}
|
|
116
|
-
cfg.max_panes_per_window = Math.floor(value);
|
|
89
|
+
const setting = findUserSetting(key);
|
|
90
|
+
if (setting !== undefined) {
|
|
91
|
+
const error = setting.setFromString(cfg, rawValue);
|
|
92
|
+
if (error !== null)
|
|
93
|
+
throw usage(error);
|
|
117
94
|
return;
|
|
118
95
|
}
|
|
119
96
|
if (topKey === 'brokerThresholds') {
|
|
@@ -133,43 +110,6 @@ function setNestedValue(cfg, key, rawValue, value) {
|
|
|
133
110
|
cfg.brokerThresholds[parts[1]] = threshold;
|
|
134
111
|
return;
|
|
135
112
|
}
|
|
136
|
-
if (key === 'completion_bell') {
|
|
137
|
-
if (typeof value !== 'boolean') {
|
|
138
|
-
throw usage(`completion_bell must be true or false`);
|
|
139
|
-
}
|
|
140
|
-
cfg.completion_bell = value;
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
if (key === 'live_cycles') {
|
|
144
|
-
if (typeof value !== 'number' || !Number.isFinite(value) || value < 1) {
|
|
145
|
-
throw usage(`live_cycles must be an integer >= 1`);
|
|
146
|
-
}
|
|
147
|
-
cfg.live_cycles = Math.floor(value);
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
if (key === 'condensed_history') {
|
|
151
|
-
if (!CONDENSED_HISTORY_MODES.includes(rawValue)) {
|
|
152
|
-
throw usage(`condensed_history must be one of: ${CONDENSED_HISTORY_MODES.join('|')}`);
|
|
153
|
-
}
|
|
154
|
-
cfg.condensed_history = rawValue;
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
if (key === 'fold_finished_tools') {
|
|
158
|
-
if (typeof value !== 'boolean') {
|
|
159
|
-
throw usage(`fold_finished_tools must be true or false`);
|
|
160
|
-
}
|
|
161
|
-
cfg.fold_finished_tools = value;
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
if (parts.length === 1) {
|
|
165
|
-
cfg[topKey] = value;
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
if (parts.length === 2 && topKey === 'auto_update') {
|
|
169
|
-
const subKey = parts[1];
|
|
170
|
-
cfg.auto_update[subKey] = value;
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
113
|
throw usage(`unsupported key path for set: ${key}`);
|
|
174
114
|
}
|
|
175
115
|
// ---------------------------------------------------------------------------
|
|
@@ -183,7 +123,7 @@ const configGet = defineLeaf({
|
|
|
183
123
|
name: 'sys config get',
|
|
184
124
|
summary: 'read a config value by dotted key',
|
|
185
125
|
params: [
|
|
186
|
-
{ kind: 'positional', name: 'key', type: 'string', required: true, constraint:
|
|
126
|
+
{ kind: 'positional', name: 'key', type: 'string', required: true, constraint: `Dotted key path. Top-level keys: ${CONFIG_TOP_LEVEL_HELP}. brokerThresholds is user-only; keybindings is readable only as the whole user-scope object and editable only through sys setup.` },
|
|
187
127
|
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Scope to read from. Default: user.' },
|
|
188
128
|
],
|
|
189
129
|
output: [
|
|
@@ -217,9 +157,9 @@ const configSet = defineLeaf({
|
|
|
217
157
|
name: 'sys config set',
|
|
218
158
|
summary: 'write a config value by dotted key',
|
|
219
159
|
params: [
|
|
220
|
-
{ kind: 'positional', name: 'key', type: 'string', required: true, constraint:
|
|
160
|
+
{ kind: 'positional', name: 'key', type: 'string', required: true, constraint: `Dotted key path. User-scope scalar settings: ${SCALAR_SETTING_HELP}. Also supported: brokerThresholds.warning and brokerThresholds.automaticReviveCap (user scope; integer >= 1), modelLadders.defaultProvider, and modelLadders.<anthropic|openai>.<ultra|strong|medium|light>. Keybindings are edited only through sys setup.` },
|
|
221
161
|
{ kind: 'flag', name: 'value', type: 'string', required: true, constraint: 'value VALUE — string, required. Stored as-is if quoted; coerced to number or boolean when unambiguous.' },
|
|
222
|
-
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Scope to write to. Default: user.' },
|
|
162
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Scope to write to. Default: user. Project scope is supported only for modelLadders.' },
|
|
223
163
|
],
|
|
224
164
|
output: [
|
|
225
165
|
{ name: 'key', type: 'string', required: true, constraint: 'Echo of input key.' },
|
|
@@ -234,6 +174,7 @@ const configSet = defineLeaf({
|
|
|
234
174
|
const rawValue = input['value'];
|
|
235
175
|
const scope = resolveScope(input['scope']);
|
|
236
176
|
assertUserScopeBrokerThresholds(key, scope);
|
|
177
|
+
assertUserSettingScope(key, scope);
|
|
237
178
|
// Flags are stringly-typed; coerce to number or boolean when unambiguous
|
|
238
179
|
const parsed = parseConfigValue(rawValue);
|
|
239
180
|
// Ladder rungs record explicit intent (even when the value equals today's
|