@north-light/crouter 0.3.220 → 0.3.221
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/api/client.d.ts +9 -0
- package/dist/api/client.js +10 -0
- package/dist/api/dto/chat-inventory.d.ts +43 -0
- package/dist/api/dto/chat-inventory.js +11 -0
- package/dist/api/dto/profiles.d.ts +19 -5
- package/dist/api/dto/profiles.js +2 -1
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/routes.d.ts +1 -0
- package/dist/api/routes.js +1 -0
- package/dist/build-root.d.ts +2 -6
- package/dist/build-root.js +51 -4
- package/dist/builtin-memory/internal/agent-shaping.md +3 -1
- package/dist/builtin-memory/internal/memory-loading.md +4 -0
- package/dist/builtin-memory/plan/roadmap.md +7 -1
- package/dist/builtin-memory/spec/guide.md +7 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crtr-commands/index.ts +7 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +7 -2
- package/dist/clients/attach/__tests__/ref-autocomplete.test.js +1 -1
- package/dist/clients/attach/__tests__/titled-editor-preview.test.js +1 -1
- package/dist/clients/attach/overlays/file-review.js +2 -2
- package/dist/clients/attach/session/keys.d.ts +1 -1
- package/dist/clients/attach/session/profile-files.js +1 -1
- package/dist/clients/attach/viewer.js +690 -690
- package/dist/clients/inbox/review/launch.d.ts +8 -4
- package/dist/clients/inbox/review/launch.js +55 -5
- package/dist/clients/inbox/review/review-client.d.ts +1 -0
- package/dist/clients/inbox/review/review-client.js +4 -0
- package/dist/clients/inbox/review-adapter.d.ts +1 -8
- package/dist/clients/inbox/review-adapter.js +4 -52
- package/dist/commands/memory/lint.js +2 -1
- package/dist/commands/memory/read.js +1 -0
- package/dist/commands/memory.js +1 -1
- package/dist/commands/pkg/market-manage.js +165 -75
- package/dist/commands/pkg/plugin-inspect.js +19 -2
- package/dist/commands/pkg/plugin-manage.d.ts +8 -3
- package/dist/commands/pkg/plugin-manage.js +72 -24
- package/dist/commands/profile/default.js +6 -10
- package/dist/commands/profile/list.js +5 -3
- package/dist/commands/profile/new.js +21 -8
- package/dist/commands/profile/project.js +25 -19
- package/dist/commands/profile/show.js +3 -3
- package/dist/commands/surface-inbox.js +1 -0
- package/dist/commands/sys/__tests__/migrate.test.js +16 -5
- package/dist/commands/sys/doctor.js +35 -5
- package/dist/commands/sys/migrate.js +38 -19
- package/dist/commands/sys/setup-core.js +1 -1
- package/dist/commands/sys/sync-project-guidance.js +1 -1
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +1 -1
- package/dist/core/__tests__/fixtures/c5-command-boundary-ext.js +24 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +24 -18
- package/dist/core/__tests__/fixtures/fake-engine.js +8 -1
- package/dist/core/__tests__/inline-memory-refs.test.js +36 -2
- package/dist/core/__tests__/profile-project-memory-delivery.test.js +217 -0
- package/dist/core/__tests__/serial/broker-sdk-wiring.test.js +102 -2
- package/dist/core/bootstrap.js +6 -0
- package/dist/core/canvas/browse/app.js +5 -2
- package/dist/core/canvas/browse/model.d.ts +25 -15
- package/dist/core/canvas/browse/model.js +86 -65
- package/dist/core/canvas/render-source.d.ts +6 -0
- package/dist/core/canvas/render-source.js +7 -1
- package/dist/core/canvas/render.js +10 -2
- package/dist/core/command-hooks/artifact.d.ts +10 -0
- package/dist/core/command-hooks/artifact.js +129 -0
- package/dist/core/command-hooks/catalog.d.ts +14 -0
- package/dist/core/command-hooks/catalog.js +38 -0
- package/dist/core/command-hooks/compose.d.ts +15 -0
- package/dist/core/command-hooks/compose.js +99 -0
- package/dist/core/command-hooks/discovery.d.ts +87 -0
- package/dist/core/command-hooks/discovery.js +174 -0
- package/dist/core/command-hooks/help.d.ts +5 -0
- package/dist/core/command-hooks/help.js +18 -0
- package/dist/core/command-hooks/index.d.ts +6 -0
- package/dist/core/command-hooks/index.js +6 -0
- package/dist/core/command-hooks/report.d.ts +23 -0
- package/dist/core/command-hooks/report.js +19 -0
- package/dist/core/command-hooks/schema.d.ts +27 -0
- package/dist/core/command-hooks/schema.js +68 -0
- package/dist/core/command-hooks/transport/exec-invoke.d.ts +22 -0
- package/dist/core/command-hooks/transport/exec-invoke.js +274 -0
- package/dist/core/command-plugins/presence.d.ts +2 -0
- package/dist/core/command-plugins/presence.js +17 -0
- package/dist/core/command-plugins/transport/exec-invoke.d.ts +5 -0
- package/dist/core/command-plugins/transport/exec-invoke.js +58 -5
- package/dist/core/command.d.ts +8 -1
- package/dist/core/command.js +12 -10
- package/dist/core/help.d.ts +7 -1
- package/dist/core/io.d.ts +9 -1
- package/dist/core/io.js +44 -2
- package/dist/core/memory/inline-ref-inventory.d.ts +2 -1
- package/dist/core/memory/inline-ref-inventory.js +15 -8
- package/dist/core/memory-resolver.d.ts +13 -1
- package/dist/core/memory-resolver.js +25 -19
- package/dist/core/profiles/manifest.d.ts +13 -2
- package/dist/core/profiles/manifest.js +84 -18
- package/dist/core/profiles/select.js +9 -9
- package/dist/core/render.js +11 -0
- package/dist/core/runtime/advertised-command-invocation.d.ts +20 -0
- package/dist/core/runtime/advertised-command-invocation.js +233 -0
- package/dist/core/runtime/bearings.js +1 -1
- package/dist/core/runtime/broker/event-projection.js +7 -0
- package/dist/core/runtime/broker/frame-dispatch.d.ts +1 -1
- package/dist/core/runtime/broker/frame-dispatch.js +15 -10
- package/dist/core/runtime/broker/read-ops.d.ts +4 -0
- package/dist/core/runtime/broker/read-ops.js +6 -2
- package/dist/core/runtime/broker-extension-render.js +1 -1
- package/dist/core/runtime/broker-inventory.d.ts +5 -0
- package/dist/core/runtime/broker-inventory.js +191 -0
- package/dist/core/runtime/broker-protocol.d.ts +9 -2
- package/dist/core/runtime/broker.js +10 -1
- package/dist/core/runtime/command-surface.d.ts +33 -0
- package/dist/core/runtime/command-surface.js +81 -0
- package/dist/core/runtime/node-read.js +5 -0
- package/dist/core/scope.d.ts +26 -1
- package/dist/core/scope.js +52 -12
- package/dist/core/substrate/on-read.d.ts +7 -1
- package/dist/core/substrate/on-read.js +13 -4
- package/dist/core/substrate/render.js +14 -5
- package/dist/core/substrate/schema.d.ts +11 -1
- package/dist/core/substrate/schema.js +11 -2
- package/dist/daemon/api/__tests__/profile-launch-gates.test.js +4 -4
- package/dist/daemon/api/handlers/chat-inventory.d.ts +2 -0
- package/dist/daemon/api/handlers/chat-inventory.js +25 -0
- package/dist/daemon/api/handlers/profiles.js +7 -1
- package/dist/daemon/api/map.d.ts +2 -1
- package/dist/daemon/api/map.js +3 -2
- package/dist/daemon/api/server.js +2 -0
- package/dist/hook-authoring.d.ts +75 -0
- package/dist/hook-authoring.js +358 -0
- package/dist/hook-process.d.ts +7 -0
- package/dist/hook-process.js +34 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/migrations/002-profile-project-memory.d.ts +2 -0
- package/dist/migrations/002-profile-project-memory.js +71 -0
- package/dist/migrations/profile-manifests.d.ts +30 -0
- package/dist/migrations/profile-manifests.js +70 -0
- package/dist/migrations/registry.js +10 -5
- package/dist/migrations/types.d.ts +28 -1
- package/dist/migrations/types.js +15 -9
- package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +21 -4
- package/dist/pi-extensions/canvas-structured-output.js +85 -2
- package/dist/types.d.ts +15 -6
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/clients/attach/__tests__/file-review-focus.test.js +0 -49
- /package/dist/{clients/attach/__tests__/file-review-focus.test.d.ts → core/__tests__/profile-project-memory-delivery.test.d.ts} +0 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// The profile-manifest lane runner: fold every registered profile-manifest
|
|
2
|
+
// migration over each stored `profile.json`, one profile at a time, under the
|
|
3
|
+
// caller-supplied per-profile lock.
|
|
4
|
+
//
|
|
5
|
+
// This lane reads RAW BYTES. The current manifest reader treats a manifest
|
|
6
|
+
// whose projects are not the current shape exactly like corrupt JSON, so a
|
|
7
|
+
// pre-migration home has no readable profiles at all — every profile surface,
|
|
8
|
+
// `listProfiles()` included, is blind until this lane has run. That is why the
|
|
9
|
+
// runner enumerates the profiles root itself, why `crtr sys migrate` runs it
|
|
10
|
+
// before any other profile read, and why its snapshots (not `listProfiles()`)
|
|
11
|
+
// are what the rest of that command's discovery works from.
|
|
12
|
+
//
|
|
13
|
+
// The lock arrives as a callback rather than an import: a migration file may
|
|
14
|
+
// not reach into the profile module, and a lane runner that did would be one
|
|
15
|
+
// import away from the same blind reader.
|
|
16
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { atomicWriteJson } from '../core/fs-utils.js';
|
|
19
|
+
import { STATE_MIGRATIONS } from './registry.js';
|
|
20
|
+
/** Frozen copy of the generated-profile-id shape (core/profiles/manifest.ts),
|
|
21
|
+
* so enumeration matches `listProfiles()`: a hand-made directory crtr can
|
|
22
|
+
* never resolve is skipped here too, rather than failing the whole run. */
|
|
23
|
+
const ID_SHAPE = /^[a-z0-9]+(?:-[a-z0-9]+)*-[0-9a-f]{8}$/;
|
|
24
|
+
export function runProfileManifestMigrations(profilesRoot, opts) {
|
|
25
|
+
const migrations = (opts.migrations ?? STATE_MIGRATIONS).filter((m) => m.lane === 'profile-manifest');
|
|
26
|
+
const result = { profiles: 0, applied: [], snapshots: [], skipped: [] };
|
|
27
|
+
if (!existsSync(profilesRoot))
|
|
28
|
+
return result;
|
|
29
|
+
for (const dirent of readdirSync(profilesRoot, { withFileTypes: true })) {
|
|
30
|
+
if (!dirent.isDirectory())
|
|
31
|
+
continue;
|
|
32
|
+
const profileId = dirent.name;
|
|
33
|
+
if (!ID_SHAPE.test(profileId))
|
|
34
|
+
continue;
|
|
35
|
+
const path = join(profilesRoot, profileId, 'profile.json');
|
|
36
|
+
if (!existsSync(path))
|
|
37
|
+
continue;
|
|
38
|
+
result.profiles += 1;
|
|
39
|
+
opts.withLock(profileId, () => {
|
|
40
|
+
let raw;
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
43
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
44
|
+
throw new Error('manifest is not a JSON object');
|
|
45
|
+
}
|
|
46
|
+
raw = parsed;
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
result.skipped.push({ profileId, path, error: (e instanceof Error ? e.message : String(e)).split('\n')[0] });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
let current = raw;
|
|
53
|
+
let changed = false;
|
|
54
|
+
for (const migration of migrations) {
|
|
55
|
+
const next = migration.apply({ profileId, path, raw: current });
|
|
56
|
+
if (next === null)
|
|
57
|
+
continue;
|
|
58
|
+
current = next;
|
|
59
|
+
changed = true;
|
|
60
|
+
result.applied.push({ profileId, path, migration: migration.description });
|
|
61
|
+
}
|
|
62
|
+
if (changed && opts.dryRun !== true)
|
|
63
|
+
atomicWriteJson(path, current);
|
|
64
|
+
// Every migration guarantees a current manifest or throws, so the folded
|
|
65
|
+
// record is the post-migration contract by construction.
|
|
66
|
+
result.snapshots.push({ profileId, path, manifest: current });
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Authoring contract (each migration is one file, `NNN-<desc>.ts`, appended
|
|
5
5
|
// here in order):
|
|
6
|
-
// • Idempotent. A convergent migration re-runs on every
|
|
7
|
-
// so applying it to
|
|
8
|
-
// journaled migration runs once per database via
|
|
9
|
-
// body must still tolerate a crash-resume re-run of
|
|
6
|
+
// • Idempotent. A convergent or profile-manifest migration re-runs on every
|
|
7
|
+
// `crtr sys migrate`, so applying it to already-migrated state must plan
|
|
8
|
+
// zero changes. A journaled migration runs once per database via
|
|
9
|
+
// user_version, but its body must still tolerate a crash-resume re-run of
|
|
10
|
+
// any filesystem work.
|
|
10
11
|
// • Journaled filesystem steps are crash-resumable: fs writes happen outside
|
|
11
12
|
// the db transaction, so a step interrupted mid-fs-work must complete
|
|
12
13
|
// (never corrupt) when the chain re-runs it.
|
|
@@ -16,4 +17,8 @@
|
|
|
16
17
|
// This keeps the registry legal in every CLI leaf's import graph (the
|
|
17
18
|
// build's V-1 gate).
|
|
18
19
|
import { surfacesFrontmatterMigration } from './001-surfaces-frontmatter.js';
|
|
19
|
-
|
|
20
|
+
import { profileProjectMemoryMigration } from './002-profile-project-memory.js';
|
|
21
|
+
export const STATE_MIGRATIONS = [
|
|
22
|
+
surfacesFrontmatterMigration,
|
|
23
|
+
profileProjectMemoryMigration,
|
|
24
|
+
];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import type { ProfileManifest } from '../types.js';
|
|
2
3
|
export interface JournaledMigration {
|
|
3
4
|
lane: 'journaled';
|
|
4
5
|
description: string;
|
|
@@ -19,7 +20,33 @@ export interface ConvergentMigration {
|
|
|
19
20
|
* runner filters no-op changes and performs the writes. */
|
|
20
21
|
apply(store: StoreSnapshot, context?: ConvergentMigrationContext): DocChange[];
|
|
21
22
|
}
|
|
22
|
-
|
|
23
|
+
/** One stored `profile.json` handed to the profile-manifest lane. */
|
|
24
|
+
export interface ProfileManifestInput {
|
|
25
|
+
/** The immediate directory name under the profiles root. The only root
|
|
26
|
+
* classifier a migration may key on — a manifest `name` is mutable. */
|
|
27
|
+
profileId: string;
|
|
28
|
+
/** Absolute path to the `profile.json`. */
|
|
29
|
+
path: string;
|
|
30
|
+
/** Parsed JSON exactly as stored, historical shapes included. */
|
|
31
|
+
raw: Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
export interface ProfileManifestMigration {
|
|
34
|
+
lane: 'profile-manifest';
|
|
35
|
+
description: string;
|
|
36
|
+
/** Pure planning: the full replacement record, or null when this manifest is
|
|
37
|
+
* already current. A structure the migration cannot convert THROWS — the
|
|
38
|
+
* run fails rather than persisting a guessed value. */
|
|
39
|
+
apply(input: ProfileManifestInput): Record<string, unknown> | null;
|
|
40
|
+
}
|
|
41
|
+
export type StateMigration = JournaledMigration | ConvergentMigration | ProfileManifestMigration;
|
|
42
|
+
/** One profile's manifest after every profile-manifest migration folded —
|
|
43
|
+
* the bytes on disk after a write run, and the bytes a write run WOULD have
|
|
44
|
+
* produced under `--dry-run`. */
|
|
45
|
+
export interface ProfileManifestSnapshot {
|
|
46
|
+
profileId: string;
|
|
47
|
+
path: string;
|
|
48
|
+
manifest: ProfileManifest;
|
|
49
|
+
}
|
|
23
50
|
/** One parsed markdown doc in a store snapshot. */
|
|
24
51
|
export interface StoreDoc {
|
|
25
52
|
/** Absolute file path. */
|
package/dist/migrations/types.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
// State-migration shapes.
|
|
1
|
+
// State-migration shapes. Three lanes, split by which store owns the state:
|
|
2
2
|
//
|
|
3
|
-
// journaled
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// convergent
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
3
|
+
// journaled — canvas-db state. Runs inside the db's ordered migration
|
|
4
|
+
// chain (core/canvas/db.ts appends these to MIGRATIONS), so
|
|
5
|
+
// each step gets the chain's BEGIN IMMEDIATE/COMMIT +
|
|
6
|
+
// user_version gating and runs exactly once per database.
|
|
7
|
+
// convergent — on-disk memory documents. No journal exists over a user's
|
|
8
|
+
// stores (files appear, sync in, get hand-edited), so these
|
|
9
|
+
// CONVERGE instead: every run re-scans and rewrites whatever
|
|
10
|
+
// still matches the old shape.
|
|
11
|
+
// profile-manifest — `~/.crouter/profiles/<id>/profile.json`. Converges like
|
|
12
|
+
// the document lane, but each manifest folds under the same
|
|
13
|
+
// per-profile lock add/remove take, and a structure the
|
|
14
|
+
// migration cannot convert throws instead of being guessed.
|
|
15
|
+
//
|
|
16
|
+
// `crtr sys migrate` runs both on-disk lanes, profile manifests first.
|
|
11
17
|
export {};
|
|
@@ -26,7 +26,7 @@ after(() => {
|
|
|
26
26
|
else
|
|
27
27
|
process.env['CRTR_NODE_ID'] = previous.nodeId;
|
|
28
28
|
});
|
|
29
|
-
test('
|
|
29
|
+
test('strict provider schema stays portable while original constraints validate submit', async () => {
|
|
30
30
|
home = mkdtempSync(join(tmpdir(), 'crtr-structured-output-'));
|
|
31
31
|
process.env['CRTR_HOME'] = home;
|
|
32
32
|
process.env['CRTR_NODE_ID'] = 'structured-output-test';
|
|
@@ -44,8 +44,14 @@ test('optional submit fields remain optional through a strict provider tool sche
|
|
|
44
44
|
},
|
|
45
45
|
required: ['summary'],
|
|
46
46
|
},
|
|
47
|
+
tags: {
|
|
48
|
+
type: 'array',
|
|
49
|
+
items: { type: 'string' },
|
|
50
|
+
minItems: 2,
|
|
51
|
+
maxItems: 5,
|
|
52
|
+
},
|
|
47
53
|
},
|
|
48
|
-
required: ['status', 'verification', 'details'],
|
|
54
|
+
required: ['status', 'verification', 'details', 'tags'],
|
|
49
55
|
};
|
|
50
56
|
writeOutputSchema('structured-output-test', 'oneoff', schema);
|
|
51
57
|
let definition;
|
|
@@ -57,15 +63,26 @@ test('optional submit fields remain optional through a strict provider tool sche
|
|
|
57
63
|
});
|
|
58
64
|
assert.ok(definition, 'the pending request registers submit');
|
|
59
65
|
const parameters = definition['parameters'];
|
|
60
|
-
assert.deepEqual(parameters.required, ['status', 'commit', 'verification', 'blocker', 'details']);
|
|
66
|
+
assert.deepEqual(parameters.required, ['status', 'commit', 'verification', 'blocker', 'details', 'tags']);
|
|
61
67
|
assert.deepEqual(parameters.properties.commit, { anyOf: [{ type: 'string' }, { type: 'null' }] });
|
|
62
68
|
assert.deepEqual(parameters.properties.blocker, { anyOf: [{ type: 'string' }, { type: 'null' }] });
|
|
63
69
|
assert.equal(parameters.additionalProperties, false);
|
|
64
70
|
assert.equal(parameters.properties.details.additionalProperties, false);
|
|
65
|
-
assert.
|
|
71
|
+
assert.equal(parameters.properties.tags['minItems'], undefined);
|
|
72
|
+
assert.equal(parameters.properties.tags['maxItems'], undefined);
|
|
73
|
+
assert.match(parameters.properties.tags['description'], /minItems: 2/);
|
|
74
|
+
assert.match(parameters.properties.tags['description'], /maxItems: 5/);
|
|
75
|
+
assert.deepEqual(schema.required, ['status', 'verification', 'details', 'tags'], 'the stored request keeps its original constraints');
|
|
66
76
|
const [providerTool] = convertResponsesTools([definition], {
|
|
67
77
|
strict: false,
|
|
68
78
|
supportsStrictMode: true,
|
|
69
79
|
});
|
|
70
80
|
assert.equal(providerTool['strict'], true);
|
|
81
|
+
const execute = definition['execute'];
|
|
82
|
+
await assert.rejects(execute('call', {
|
|
83
|
+
status: 'done',
|
|
84
|
+
verification: 'verified',
|
|
85
|
+
details: { summary: 'ok' },
|
|
86
|
+
tags: ['only-one'],
|
|
87
|
+
}, new AbortController().signal), /must not have fewer than 2 items/);
|
|
71
88
|
});
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// stop-guard will reprompt the node with an explicit error about the invalid file.
|
|
6
6
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { dirname } from 'node:path';
|
|
8
|
+
import { validateToolArguments } from '@earendil-works/pi-ai';
|
|
8
9
|
import { emitEvent } from '../core/events/emit.js';
|
|
9
10
|
import { publishBrokerReport } from '../core/runtime/broker/daemon-ops.js';
|
|
10
11
|
import { outputResultPath, readOutputRequest, removeOutputSchema, } from '../core/runtime/structured-output.js';
|
|
@@ -30,6 +31,85 @@ function retireTool(pi) {
|
|
|
30
31
|
/* best-effort; the execute handler is inert when no schema file exists */
|
|
31
32
|
}
|
|
32
33
|
}
|
|
34
|
+
const STRICT_STRING_FORMATS = new Set([
|
|
35
|
+
'date-time', 'time', 'date', 'duration', 'email', 'hostname', 'uri', 'ipv4', 'ipv6', 'uuid',
|
|
36
|
+
]);
|
|
37
|
+
// Provider strict modes accept a structural subset; omitted constraints stay visible in descriptions.
|
|
38
|
+
function providerStrictSchema(schema) {
|
|
39
|
+
const transform = (value) => {
|
|
40
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
41
|
+
return value;
|
|
42
|
+
const source = { ...value };
|
|
43
|
+
const take = (key) => {
|
|
44
|
+
const child = source[key];
|
|
45
|
+
delete source[key];
|
|
46
|
+
return child;
|
|
47
|
+
};
|
|
48
|
+
const transformed = {};
|
|
49
|
+
const ref = take('$ref');
|
|
50
|
+
if (ref !== undefined)
|
|
51
|
+
return { $ref: ref };
|
|
52
|
+
const defs = take('$defs');
|
|
53
|
+
if (defs !== null && typeof defs === 'object' && !Array.isArray(defs)) {
|
|
54
|
+
transformed['$defs'] = Object.fromEntries(Object.entries(defs).map(([name, definition]) => [name, transform(definition)]));
|
|
55
|
+
}
|
|
56
|
+
const type = take('type');
|
|
57
|
+
const anyOf = take('anyOf');
|
|
58
|
+
const oneOf = take('oneOf');
|
|
59
|
+
const allOf = take('allOf');
|
|
60
|
+
if (Array.isArray(anyOf))
|
|
61
|
+
transformed['anyOf'] = anyOf.map(transform);
|
|
62
|
+
else if (Array.isArray(oneOf))
|
|
63
|
+
transformed['anyOf'] = oneOf.map(transform);
|
|
64
|
+
else if (Array.isArray(allOf))
|
|
65
|
+
transformed['allOf'] = allOf.map(transform);
|
|
66
|
+
else if (type !== undefined)
|
|
67
|
+
transformed['type'] = type;
|
|
68
|
+
else
|
|
69
|
+
return value;
|
|
70
|
+
const description = take('description');
|
|
71
|
+
if (description !== undefined)
|
|
72
|
+
transformed['description'] = description;
|
|
73
|
+
const title = take('title');
|
|
74
|
+
if (title !== undefined)
|
|
75
|
+
transformed['title'] = title;
|
|
76
|
+
if (type === 'object') {
|
|
77
|
+
const properties = take('properties');
|
|
78
|
+
transformed['properties'] = properties !== null && typeof properties === 'object' && !Array.isArray(properties)
|
|
79
|
+
? Object.fromEntries(Object.entries(properties).map(([name, property]) => [name, transform(property)]))
|
|
80
|
+
: {};
|
|
81
|
+
take('additionalProperties');
|
|
82
|
+
transformed['additionalProperties'] = false;
|
|
83
|
+
const required = take('required');
|
|
84
|
+
if (required !== undefined)
|
|
85
|
+
transformed['required'] = required;
|
|
86
|
+
}
|
|
87
|
+
else if (type === 'string') {
|
|
88
|
+
const format = take('format');
|
|
89
|
+
if (typeof format === 'string' && STRICT_STRING_FORMATS.has(format))
|
|
90
|
+
transformed['format'] = format;
|
|
91
|
+
else if (format !== undefined)
|
|
92
|
+
source['format'] = format;
|
|
93
|
+
}
|
|
94
|
+
else if (type === 'array') {
|
|
95
|
+
const items = take('items');
|
|
96
|
+
if (items !== undefined)
|
|
97
|
+
transformed['items'] = transform(items);
|
|
98
|
+
const minItems = take('minItems');
|
|
99
|
+
if (minItems === 0 || minItems === 1)
|
|
100
|
+
transformed['minItems'] = minItems;
|
|
101
|
+
else if (minItems !== undefined)
|
|
102
|
+
source['minItems'] = minItems;
|
|
103
|
+
}
|
|
104
|
+
if (Object.keys(source).length > 0) {
|
|
105
|
+
const existing = typeof transformed['description'] === 'string' ? transformed['description'] : '';
|
|
106
|
+
const constraints = `{${Object.entries(source).map(([key, child]) => `${key}: ${JSON.stringify(child)}`).join(', ')}}`;
|
|
107
|
+
transformed['description'] = existing === '' ? constraints : `${existing}\n\n${constraints}`;
|
|
108
|
+
}
|
|
109
|
+
return transformed;
|
|
110
|
+
};
|
|
111
|
+
return transform(schema);
|
|
112
|
+
}
|
|
33
113
|
function strictToolSchema(schema) {
|
|
34
114
|
const transform = (value) => {
|
|
35
115
|
if (Array.isArray(value))
|
|
@@ -61,7 +141,10 @@ function strictToolSchema(schema) {
|
|
|
61
141
|
transformed['required'] = Object.keys(properties);
|
|
62
142
|
return transformed;
|
|
63
143
|
};
|
|
64
|
-
return transform(schema);
|
|
144
|
+
return providerStrictSchema(transform(schema));
|
|
145
|
+
}
|
|
146
|
+
function validateResult(value, schema) {
|
|
147
|
+
return validateToolArguments({ name: 'submit', description: '', parameters: schema }, { type: 'toolCall', id: 'structured-output', name: 'submit', arguments: value });
|
|
65
148
|
}
|
|
66
149
|
function omitStrictNulls(value, schema) {
|
|
67
150
|
if (value === null || typeof value !== 'object' || Array.isArray(value) || schema === null || typeof schema !== 'object' || Array.isArray(schema))
|
|
@@ -156,7 +239,7 @@ export function registerCanvasStructuredOutput(pi) {
|
|
|
156
239
|
details: { submitted: false, reason: 'schema_replaced' },
|
|
157
240
|
};
|
|
158
241
|
}
|
|
159
|
-
const result = omitStrictNulls(params, current.request.schema);
|
|
242
|
+
const result = validateResult(omitStrictNulls(params, current.request.schema), current.request.schema);
|
|
160
243
|
const resultPath = outputResultPath(nodeId);
|
|
161
244
|
const json = JSON.stringify(result, null, 2);
|
|
162
245
|
mkdirSync(dirname(resultPath), { recursive: true });
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BindingId } from './core/keybindings/catalog.js';
|
|
2
|
+
import type { ProfileProject } from './api/dto/profiles.js';
|
|
2
3
|
export type Scope = 'user' | 'project' | 'builtin';
|
|
3
4
|
export declare const ExitCode: {
|
|
4
5
|
readonly SUCCESS: 0;
|
|
@@ -94,6 +95,13 @@ export interface PluginManifest {
|
|
|
94
95
|
* crtr owns parse/help/render/errors and direct-spawns the plugin's one
|
|
95
96
|
* executable per leaf invocation. See `src/core/command-plugins/`. */
|
|
96
97
|
commands?: string;
|
|
98
|
+
/** Plugin-root-relative path to a declarative hook manifest (`hooks.json`).
|
|
99
|
+
* Must be declared together with `hookExecutable`; hooks are trusted local
|
|
100
|
+
* code and are unavailable to endpoint archive plugins. */
|
|
101
|
+
hooks?: string;
|
|
102
|
+
/** Plugin-root-relative executable that dispatches declarations in `hooks`.
|
|
103
|
+
* Must be a regular file with a POSIX executable permission bit. */
|
|
104
|
+
hookExecutable?: string;
|
|
97
105
|
/** Bare-callable executables this plugin ships, keyed by the bare command
|
|
98
106
|
* name a node's bash sees. Each value is a PLUGIN-ROOT-relative path to a
|
|
99
107
|
* regular executable file when the effective set resolves. These are trusted
|
|
@@ -411,9 +419,9 @@ export interface InstalledMarketplace {
|
|
|
411
419
|
}
|
|
412
420
|
/** The on-disk manifest at `~/.crouter/profiles/<slug>-<id>/profile.json`
|
|
413
421
|
* (spec §2.1). A profile is an agent identity defined at the user root: it
|
|
414
|
-
* names N project directories and owns its own `memory/` store.
|
|
415
|
-
* are absolute, real-path-resolved directories in manifest order —
|
|
416
|
-
* pointer set `findProjectScopeRoots` walks, after the node's own
|
|
422
|
+
* names N project directories and owns its own `memory/` store. Project
|
|
423
|
+
* `path`s are absolute, real-path-resolved directories in manifest order —
|
|
424
|
+
* the pointer set `findProjectScopeRoots` walks, after the node's own
|
|
417
425
|
* cwd. `name` is the mutable display name (and the gateable `profile` subject
|
|
418
426
|
* field); `<slug>-<id>` in the directory name is the stable id and
|
|
419
427
|
* never changes on rename. See `src/core/profiles/manifest.ts` for every
|
|
@@ -422,10 +430,11 @@ export interface InstalledMarketplace {
|
|
|
422
430
|
export interface ProfileManifest {
|
|
423
431
|
schema_version: number;
|
|
424
432
|
name: string;
|
|
425
|
-
projects:
|
|
433
|
+
projects: ProfileProject[];
|
|
426
434
|
/** The directory nodes under this profile are pinned to (see
|
|
427
|
-
* `profileHome`). Normalized on read to the first project when the
|
|
428
|
-
* carries none, so a profile written before homes existed still
|
|
435
|
+
* `profileHome`). Normalized on read to the first project's path when the
|
|
436
|
+
* manifest carries none, so a profile written before homes existed still
|
|
437
|
+
* has one. */
|
|
429
438
|
home: string | null;
|
|
430
439
|
/** ISO timestamp while the profile is inert; null (or absent on older manifests) means active. */
|
|
431
440
|
paused_at: string | null;
|
package/package.json
CHANGED
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/crouter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.221",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/crouter",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.221",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { fileURLToPath } from 'node:url';
|
|
2
|
-
import { test } from 'node:test';
|
|
3
|
-
import assert from 'node:assert/strict';
|
|
4
|
-
import { initTheme } from '@earendil-works/pi-coding-agent';
|
|
5
|
-
import { InputController } from '../input/controller.js';
|
|
6
|
-
import { createViewerCapabilities } from '../input/capabilities.js';
|
|
7
|
-
initTheme(undefined, false);
|
|
8
|
-
function fakeEditor() {
|
|
9
|
-
const obj = {
|
|
10
|
-
onSubmit: undefined,
|
|
11
|
-
onEscape: undefined,
|
|
12
|
-
onPasteImage: undefined,
|
|
13
|
-
onAction: () => { },
|
|
14
|
-
getText: () => '',
|
|
15
|
-
setText: () => { },
|
|
16
|
-
addToHistory: () => { },
|
|
17
|
-
insertTextAtCursor: () => { },
|
|
18
|
-
};
|
|
19
|
-
return obj;
|
|
20
|
-
}
|
|
21
|
-
test('choosing a review file closes the inline picker before the review surface takes focus', () => {
|
|
22
|
-
const editor = fakeEditor();
|
|
23
|
-
let focused = null;
|
|
24
|
-
let mountedPicker;
|
|
25
|
-
const tui = {
|
|
26
|
-
requestRender: () => { },
|
|
27
|
-
setFocus: (component) => { focused = component; },
|
|
28
|
-
showOverlay: () => ({ hide: () => { } }),
|
|
29
|
-
start: () => { },
|
|
30
|
-
stop: () => { },
|
|
31
|
-
};
|
|
32
|
-
const reviewSurface = {
|
|
33
|
-
render: () => [],
|
|
34
|
-
invalidate: () => { },
|
|
35
|
-
handleInput: () => { },
|
|
36
|
-
};
|
|
37
|
-
const controller = new InputController(tui, editor, {}, {
|
|
38
|
-
onCommand: () => { },
|
|
39
|
-
onDialogResponse: () => { },
|
|
40
|
-
onMountPicker: (component) => { mountedPicker = component; },
|
|
41
|
-
onUnmountPicker: () => { },
|
|
42
|
-
}, createViewerCapabilities({ role: 'controller', remote: false, nodeId: 'test', profileId: undefined }));
|
|
43
|
-
controller.openFileReviewPicker([fileURLToPath(import.meta.url)], process.cwd(), () => {
|
|
44
|
-
tui.setFocus(reviewSurface);
|
|
45
|
-
});
|
|
46
|
-
assert.ok(mountedPicker?.handleInput);
|
|
47
|
-
mountedPicker.handleInput('\n');
|
|
48
|
-
assert.equal(focused, reviewSurface, 'picker close must not restore editor focus over the review surface');
|
|
49
|
-
});
|