@m4ike1/ion-cue 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +92 -0
  2. package/dist/dispatcher.d.ts +31 -0
  3. package/dist/dispatcher.d.ts.map +1 -0
  4. package/dist/dispatcher.js +230 -0
  5. package/dist/dispatcher.js.map +1 -0
  6. package/dist/harness-bridge.d.ts +13 -0
  7. package/dist/harness-bridge.d.ts.map +1 -0
  8. package/dist/harness-bridge.js +29 -0
  9. package/dist/harness-bridge.js.map +1 -0
  10. package/dist/harness-router.d.ts +12 -0
  11. package/dist/harness-router.d.ts.map +1 -0
  12. package/dist/harness-router.js +189 -0
  13. package/dist/harness-router.js.map +1 -0
  14. package/dist/index.d.ts +16 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +12 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/injector.d.ts +23 -0
  19. package/dist/injector.d.ts.map +1 -0
  20. package/dist/injector.js +62 -0
  21. package/dist/injector.js.map +1 -0
  22. package/dist/marks.d.ts +26 -0
  23. package/dist/marks.d.ts.map +1 -0
  24. package/dist/marks.js +59 -0
  25. package/dist/marks.js.map +1 -0
  26. package/dist/messages.d.ts +23 -0
  27. package/dist/messages.d.ts.map +1 -0
  28. package/dist/messages.js +2 -0
  29. package/dist/messages.js.map +1 -0
  30. package/dist/registry.d.ts +94 -0
  31. package/dist/registry.d.ts.map +1 -0
  32. package/dist/registry.js +582 -0
  33. package/dist/registry.js.map +1 -0
  34. package/dist/resolver.d.ts +15 -0
  35. package/dist/resolver.d.ts.map +1 -0
  36. package/dist/resolver.js +50 -0
  37. package/dist/resolver.js.map +1 -0
  38. package/dist/scanner.d.ts +32 -0
  39. package/dist/scanner.d.ts.map +1 -0
  40. package/dist/scanner.js +252 -0
  41. package/dist/scanner.js.map +1 -0
  42. package/dist/scope.d.ts +25 -0
  43. package/dist/scope.d.ts.map +1 -0
  44. package/dist/scope.js +180 -0
  45. package/dist/scope.js.map +1 -0
  46. package/dist/secret-store.d.ts +16 -0
  47. package/dist/secret-store.d.ts.map +1 -0
  48. package/dist/secret-store.js +89 -0
  49. package/dist/secret-store.js.map +1 -0
  50. package/dist/stream-events.d.ts +26 -0
  51. package/dist/stream-events.d.ts.map +1 -0
  52. package/dist/stream-events.js +2 -0
  53. package/dist/stream-events.js.map +1 -0
  54. package/dist/toml.d.ts +3 -0
  55. package/dist/toml.d.ts.map +1 -0
  56. package/dist/toml.js +117 -0
  57. package/dist/toml.js.map +1 -0
  58. package/dist/types.d.ts +182 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/dist/types.js +2 -0
  61. package/dist/types.js.map +1 -0
  62. package/dist/validation.d.ts +5 -0
  63. package/dist/validation.d.ts.map +1 -0
  64. package/dist/validation.js +61 -0
  65. package/dist/validation.js.map +1 -0
  66. package/package.json +23 -0
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # @m4ike1/ion-cue
2
+
3
+ Behavior-steering preprocessor: scans user messages for cue directives, scope statements, and marks, then resolves them into prompt injections and harness actions.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @m4ike1/ion-cue
9
+ ```
10
+
11
+ Entrypoint: `@m4ike1/ion-cue` → `src/index.ts`.
12
+
13
+ ## Quick start
14
+
15
+ ```typescript
16
+ import { cueDispatcher } from "@m4ike1/ion-cue";
17
+
18
+ const dispatcher = cueDispatcher({ projectRoot: process.cwd(), skipHome: true });
19
+ const result = dispatcher.process("Review {@src/foo.ts} [Review: Technical]");
20
+
21
+ for (const injection of result.injections) console.log(injection.text);
22
+ if (result.error) console.error(result.error);
23
+ for (const action of result.harnessActions) console.log(action.handler, action.args);
24
+ console.log(result.rewrittenInput);
25
+ ```
26
+
27
+ ## Syntax
28
+
29
+ | Form | Example | Meaning |
30
+ |------|---------|---------|
31
+ | Cue directive | `[Review: Technical > Brief]` | Behavioral steering: element `Review`, tags `Technical`, `Brief`. Max 3 tags. Text stays in the message; behavior is injected alongside. |
32
+ | File scope | `{@src/foo.ts}`, `{src/foo.ts:10-20}` | Inject file content. Range forms: `10-20`, `10-`, `-20`, `10` (single line). `@` optional. |
33
+ | Glob scope | `{@src/*.ts}` | Inject every match, one `--- path ---` block per file. |
34
+ | Directory scope | `{@src/}` | Injects the directory's absolute path only. |
35
+ | Mark definition | own-line `{#id}` + content lines | Defines a reusable content block; content is stripped from the message and injected as `--- #id ---\n<content>`. |
36
+ | Mark reference | `{#id}` (own-line header with no content, or inline) | Injects the matching mark's content. Errors if undefined. |
37
+ | Skill reference | `{$name}` | Passed through; expanded by agent-session, not cue. |
38
+ | System command | `:quit arg` (first line starts with `:`) | Short-circuits: returns a `harness::quit` harness action, no injections. |
39
+ | Alias | `/quick` (first line starts with `/`) | Expanded from `cue.toml` `[aliases]` or `DispatcherOptions.aliases` before scanning. Unknown aliases are ignored. |
40
+
41
+ Directives inside fenced code blocks (```` ``` ````, `~~~`) and inline `` `code` `` are not scanned. Mark content blocks are also excluded from scanning.
42
+
43
+ ## Pipeline
44
+
45
+ `process(input)` runs: alias expansion → `collectMarks` → `scanDirectives` (with mark spans excluded) → system-nav short-circuit → element resolution (`resolveDirective`) → `buildAdditionalContext` / `buildScopeInjection` → position-ordered, content-deduped injections + `rewrittenInput`.
46
+
47
+ - Unknown elements are errors (`Element 'X' not defined`), except known harness names (`theme`), which route as harness actions.
48
+ - `class: "harness"` elements route as harness actions (`element.handler`), never injections.
49
+ - Duplicate marks and undefined mark references are errors. Missing files / empty globs inject `[warning: ...]` markers instead of failing.
50
+ - Identical directives coalesce; same element with distinct tag chains resolves independently.
51
+ - `refresh(disabledElements?)` re-runs discovery and reloads aliases without restart.
52
+
53
+ ## Element layout
54
+
55
+ Elements are discovered from `~/.ion/agent/cue/elements/` (user scope, unless `skipHome`) and `<projectRoot>/.ion/cue/elements/` (project scope), plus `additionalRoots`. Each element is a `<name>.toml` + `<name>.md` pair, optionally nested under an author directory. Shared tag bodies live in sibling `tags/*.md` and are used via `[[uses]]` fallback.
56
+
57
+ ```toml
58
+ name = "Review"
59
+ description = "Review code"
60
+ version = "1.0.0"
61
+ class = "model" # or "harness" (requires handler)
62
+
63
+ [tags.technical]
64
+ description = "Technical review"
65
+ overrides = ["tone"]
66
+ exclusive = false # when true, suppresses the Default Behavior section
67
+ ```
68
+
69
+ ```markdown
70
+ ## Default Behavior
71
+ Review the code thoroughly.
72
+
73
+ ## Tag: Technical
74
+ Focus on technical correctness.
75
+ ```
76
+
77
+ Aliases live in `cue.toml` next to the elements roots:
78
+
79
+ ```toml
80
+ [aliases]
81
+ quick = "[Review: Technical]"
82
+ ```
83
+
84
+ ## Docs
85
+
86
+ - [Reference](docs/reference.md) — full API of every exported symbol.
87
+ - [How-to](docs/how-to.md) — setup and common tasks.
88
+ - [Explanation](docs/explanation.md) — why cues, scopes, and marks are separate.
89
+
90
+ ## License
91
+
92
+ MIT
@@ -0,0 +1,31 @@
1
+ import { CueRegistry } from "./registry.ts";
2
+ import type { DispatchResult } from "./types.ts";
3
+ export interface DispatcherOptions {
4
+ projectRoot?: string;
5
+ additionalRoots?: string[];
6
+ skipHome?: boolean;
7
+ onDebug?: (message: string) => void;
8
+ aliases?: Record<string, string>;
9
+ /** Lowercased element names to skip at discovery (config UI toggles). */
10
+ disabledElements?: string[];
11
+ }
12
+ export declare function cueDispatcher(options?: DispatcherOptions): {
13
+ /**
14
+ * Re-run discovery and reload cue.toml aliases so elements, tags,
15
+ * and shortcuts added after startup work without a restart.
16
+ * The registry object (and every holder of it) stays valid.
17
+ */
18
+ refresh(disabledElements?: string[] | undefined): void;
19
+ process(input: string, _promptText?: string | undefined): DispatchResult & {
20
+ rewrittenInput?: string | undefined;
21
+ };
22
+ getRegistry(): CueRegistry;
23
+ getOnDebug(): ((message: string) => void) | undefined;
24
+ };
25
+ export declare function additionalContextFromDispatchResult(result: DispatchResult): string[];
26
+ export declare function beforeTurn(message: string, _session: {
27
+ mode: string;
28
+ activeElements: Map<string, unknown>;
29
+ overrides: Map<string, string>;
30
+ }, dispatcher: ReturnType<typeof cueDispatcher>): DispatchResult;
31
+ //# sourceMappingURL=dispatcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../src/dispatcher.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAG5C,OAAO,KAAK,EAAgB,cAAc,EAA4B,MAAM,YAAY,CAAC;AAEzF,MAAM,WAAW,iBAAiB;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,yEAAyE;IACzE,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC5B;AAWD,wBAAgB,aAAa,CAAC,OAAO,GAAE,iBAAsB;IAiB3D;;;;OAIG;;;;;;;EAmLJ;AAED,wBAAgB,mCAAmC,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,EAAE,CAEpF;AAED,wBAAgB,UAAU,CACzB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,EAChG,UAAU,EAAE,UAAU,CAAC,OAAO,aAAa,CAAC,GAC1C,cAAc,CAEhB","sourcesContent":["import { buildAdditionalContext, buildScopeInjection } from \"./injector.ts\";\nimport { collectMarks } from \"./marks.ts\";\nimport { CueRegistry } from \"./registry.ts\";\nimport { resolveDirective } from \"./resolver.ts\";\nimport { scanDirectives, stripSpans } from \"./scanner.ts\";\nimport type { CueDirective, DispatchResult, HarnessAction, Injection } from \"./types.ts\";\n\nexport interface DispatcherOptions {\n\tprojectRoot?: string;\n\tadditionalRoots?: string[];\n\tskipHome?: boolean;\n\tonDebug?: (message: string) => void;\n\taliases?: Record<string, string>;\n\t/** Lowercased element names to skip at discovery (config UI toggles). */\n\tdisabledElements?: string[];\n}\n\ninterface CoalescedGroup {\n\telement: string;\n\tversion?: string;\n\ttags: string[];\n\tdirectives: CueDirective[];\n\t/** Earliest character offset among the group's directives, for injection ordering. */\n\tposition: number;\n}\n\nexport function cueDispatcher(options: DispatcherOptions = {}) {\n\tconst registry = new CueRegistry(options.additionalRoots, { skipHome: options.skipHome });\n\tconst discoverDisabled = (): void => {\n\t\tregistry.discover(options.projectRoot, { disabled: options.disabledElements });\n\t};\n\tdiscoverDisabled();\n\tconst explicitAliases = options.aliases;\n\tconst loadFileAliases = (): Record<string, string> => {\n\t\ttry {\n\t\t\treturn registry.loadAliases(options.projectRoot);\n\t\t} catch {\n\t\t\treturn {};\n\t\t}\n\t};\n\tlet liveAliases: Record<string, string> = { ...loadFileAliases(), ...explicitAliases };\n\n\treturn {\n\t\t/**\n\t\t * Re-run discovery and reload cue.toml aliases so elements, tags,\n\t\t * and shortcuts added after startup work without a restart.\n\t\t * The registry object (and every holder of it) stays valid.\n\t\t */\n\t\trefresh(disabledElements?: string[]): void {\n\t\t\tif (disabledElements !== undefined) options.disabledElements = disabledElements;\n\t\t\tdiscoverDisabled();\n\t\t\tliveAliases = { ...loadFileAliases(), ...explicitAliases };\n\t\t},\n\t\tprocess(input: string, _promptText?: string): DispatchResult & { rewrittenInput?: string } {\n\t\t\t// Expand aliases: /name -> configured Cue string, then re-scan.\n\t\t\tlet workingInput = input;\n\t\t\tif (Object.keys(liveAliases).length > 0) {\n\t\t\t\tconst { directives: pre } = scanDirectives(input);\n\t\t\t\tfor (const dir of pre) {\n\t\t\t\t\tif (dir.kind === \"alias\") {\n\t\t\t\t\t\tconst expansion = liveAliases[dir.command.command];\n\t\t\t\t\t\tif (expansion) {\n\t\t\t\t\t\t\tworkingInput = workingInput.replace(`/${dir.command.command}`, expansion);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Collect marks first: their content blocks must not be scanned as\n\t\t\t// directives, and their content is stripped from the message.\n\t\t\tconst marks = collectMarks(workingInput);\n\t\t\tconst markContentSpans = marks.map((m) => ({ start: m.contentStart, end: m.contentEnd }));\n\t\t\tconst { directives, errors: scanErrors } = scanDirectives(workingInput, markContentSpans);\n\t\t\tconst injections: Injection[] = [];\n\t\t\tconst harnessActions: HarnessAction[] = [];\n\t\t\tlet error: string | undefined;\n\n\t\t\t// Surface scan errors (malformed directives)\n\t\t\tif (scanErrors.length > 0) {\n\t\t\t\terror = scanErrors.map((e) => e.message).join(\"\\n\");\n\t\t\t}\n\n\t\t\t// Separate cue directives from system-nav/alias\n\t\t\tconst cueDirectives = directives.filter((d) => d.kind === \"cue\").map((d) => d.directive);\n\t\t\tconst nonCueDirectives = directives.filter((d) => d.kind !== \"cue\");\n\n\t\t\t// Handle system-nav: route as harness actions and short-circuit.\n\t\t\t// The colon tier terminates entirely in the harness -- the model never sees it.\n\t\t\tfor (const dir of nonCueDirectives) {\n\t\t\t\tif (dir.kind === \"system-nav\") {\n\t\t\t\t\tharnessActions.push({\n\t\t\t\t\t\telement: dir.command.command,\n\t\t\t\t\t\thandler: `harness::${dir.command.command}`,\n\t\t\t\t\t\targs: dir.command.arg ? { arg: dir.command.arg } : {},\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\t// Alias directives from scanDirectives are already expanded above;\n\t\t\t\t// any remaining are unresolved and ignored.\n\t\t\t}\n\t\t\t// If system-nav was found, short-circuit: no cue processing, no injections.\n\t\t\tif (harnessActions.length > 0) {\n\t\t\t\treturn { injections: [], harnessActions, error };\n\t\t\t}\n\n\t\t\t// Duplicate marks are an error; resolve references against the map.\n\t\t\tconst marksById = new Map<string, (typeof marks)[number]>();\n\t\t\tfor (const mark of marks) {\n\t\t\t\tif (marksById.has(mark.id) && error === undefined) {\n\t\t\t\t\terror = `Duplicate mark: #${mark.id}`;\n\t\t\t\t}\n\t\t\t\tmarksById.set(mark.id, mark);\n\t\t\t}\n\t\t\tconst markHeaders = new Set(marks.map((m) => m.startIndex));\n\n\t\t\t// Coalesce cue directives by element (cues are purely behavioral now)\n\t\t\tconst groups = coalesceDirectives(cueDirectives);\n\n\t\t\t// Position-ordered pending injections, interleaving cue behavior\n\t\t\t// with scope statements by their offset in the message.\n\t\t\tconst pending: Array<{ position: number; text: string }> = [];\n\n\t\t\tfor (const group of groups) {\n\t\t\t\tif (group.tags.length > 3) {\n\t\t\t\t\tif (error === undefined) error = \"Maximum 3 tags per directive\";\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst resolved = resolveDirective({ ...group.directives[0], tags: group.tags }, registry);\n\t\t\t\tif (!resolved) {\n\t\t\t\t\t// Unknown element. Only known built-in harness commands (e.g. Theme)\n\t\t\t\t\t// are routed as harness actions; everything else is a real error.\n\t\t\t\t\tconst KNOWN_HARNESS = new Set([\"theme\"]);\n\t\t\t\t\tif (KNOWN_HARNESS.has(group.element.toLowerCase())) {\n\t\t\t\t\t\tharnessActions.push({\n\t\t\t\t\t\t\telement: group.element,\n\t\t\t\t\t\t\thandler: `harness::${group.element.toLowerCase()}`,\n\t\t\t\t\t\t\targs: { arg: group.tags.join(\" \") },\n\t\t\t\t\t\t});\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif (error === undefined) error = `Element '${group.element}' not defined`;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (resolved.element.class === \"harness\") {\n\t\t\t\t\tharnessActions.push({\n\t\t\t\t\t\telement: group.element,\n\t\t\t\t\t\thandler: resolved.element.handler ?? \"\",\n\t\t\t\t\t\targs: {},\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tfor (const text of buildAdditionalContext(resolved)) {\n\t\t\t\t\tpending.push({ position: group.position, text });\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Standalone scope statements (excluding mark headers themselves).\n\t\t\tfor (const dir of nonCueDirectives) {\n\t\t\t\tif (dir.kind !== \"scope-only\") continue;\n\t\t\t\tconst st = dir.statement;\n\n\t\t\t\t// Skill scopes ({$name}) are handled by agent-session skill expansion,\n\t\t\t\t// not cue injection — deliberately never pushed here.\n\t\t\t\tif (st.scope.type === \"id\") {\n\t\t\t\t\tif (markHeaders.has(st.position)) continue;\n\t\t\t\t\tconst mark = marksById.get(st.scope.value);\n\t\t\t\t\tif (!mark) {\n\t\t\t\t\t\tif (error === undefined) error = `Undefined mark reference: #${st.scope.value}`;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tpending.push({ position: st.position, text: `--- #${mark.id} ---\\n${mark.content}` });\n\t\t\t\t} else if (st.scope.type !== \"skill\") {\n\t\t\t\t\tpending.push({ position: st.position, text: buildScopeInjection(st.scope, options.projectRoot, marks) });\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Mark content injections, positioned at their headers.\n\t\t\tfor (const mark of marks) {\n\t\t\t\tpending.push({ position: mark.startIndex, text: `--- #${mark.id} ---\\n${mark.content}` });\n\t\t\t}\n\n\t\t\tpending.sort((a, b) => a.position - b.position);\n\t\t\t// Dedupe identical injections (e.g. a mark injected at its header\n\t\t\t// and at its reference site) by content.\n\t\t\tconst seenTexts = new Set<string>();\n\t\t\tfor (const p of pending) {\n\t\t\t\tif (seenTexts.has(p.text)) continue;\n\t\t\t\tseenTexts.add(p.text);\n\t\t\t\tinjections.push({ text: p.text, scope: null });\n\t\t\t}\n\n\t\t\tconst result: DispatchResult & { rewrittenInput?: string } = {\n\t\t\t\tinjections,\n\t\t\t\tharnessActions,\n\t\t\t\terror,\n\t\t\t};\n\n\t\t\t// Strip mark content from the message: it is delivered verbatim\n\t\t\t// via injections, so the model must not see it twice. Cue\n\t\t\t// directives are deliberately preserved: the bracketed text is\n\t\t\t// part of the user's message (e.g. a style suffix like\n\t\t\t// \"[Answer: brief > human]\") even though its behavior is\n\t\t\t// injected alongside.\n\t\t\tconst allStrippedSpans = [...markContentSpans].sort((a, b) => a.start - b.start);\n\t\t\tif (allStrippedSpans.length > 0) {\n\t\t\t\tresult.rewrittenInput = stripSpans(workingInput, allStrippedSpans);\n\t\t\t} else {\n\t\t\t\t// Nothing stripped (cue text is preserved): still report the\n\t\t\t\t// message the model sees. stripSpans is skipped because it\n\t\t\t\t// collapses whitespace even with no spans.\n\t\t\t\tresult.rewrittenInput = workingInput;\n\t\t\t}\n\n\t\t\treturn result;\n\t\t},\n\n\t\tgetRegistry() {\n\t\t\treturn registry;\n\t\t},\n\n\t\tgetOnDebug() {\n\t\t\treturn options.onDebug;\n\t\t},\n\t};\n}\n\nexport function additionalContextFromDispatchResult(result: DispatchResult): string[] {\n\treturn result.injections.map((inj) => inj.text);\n}\n\nexport function beforeTurn(\n\tmessage: string,\n\t_session: { mode: string; activeElements: Map<string, unknown>; overrides: Map<string, string> },\n\tdispatcher: ReturnType<typeof cueDispatcher>,\n): DispatchResult {\n\treturn dispatcher.process(message);\n}\n\n/**\n * Coalesce duplicate directives.\n * Directives merge only when they name the same element, version, and tag\n * chain; distinct tag chains resolve independently so `[a: x] [a: y]`\n * injects two behavioral contexts instead of pooling tags into one\n * over-capped directive.\n */\nfunction coalesceDirectives(directives: CueDirective[]): CoalescedGroup[] {\n\tconst groups = new Map<string, CoalescedGroup>();\n\n\tfor (const dir of directives) {\n\t\tconst key = `${dir.element.toLowerCase()}::${dir.version ?? \"\"}::${dir.tags.map((t) => t.toLowerCase()).join(\">\")}`;\n\n\t\tconst existing = groups.get(key);\n\t\tif (existing) {\n\t\t\texisting.directives.push(dir);\n\t\t\texisting.position = Math.min(existing.position, dir.position ?? 0);\n\t\t} else {\n\t\t\tgroups.set(key, {\n\t\t\t\telement: dir.element,\n\t\t\t\tversion: dir.version,\n\t\t\t\ttags: [...dir.tags],\n\t\t\t\tdirectives: [dir],\n\t\t\t\tposition: dir.position ?? 0,\n\t\t\t});\n\t\t}\n\t}\n\n\treturn Array.from(groups.values());\n}\n"]}
@@ -0,0 +1,230 @@
1
+ import { buildAdditionalContext, buildScopeInjection } from "./injector.js";
2
+ import { collectMarks } from "./marks.js";
3
+ import { CueRegistry } from "./registry.js";
4
+ import { resolveDirective } from "./resolver.js";
5
+ import { scanDirectives, stripSpans } from "./scanner.js";
6
+ export function cueDispatcher(options = {}) {
7
+ const registry = new CueRegistry(options.additionalRoots, { skipHome: options.skipHome });
8
+ const discoverDisabled = () => {
9
+ registry.discover(options.projectRoot, { disabled: options.disabledElements });
10
+ };
11
+ discoverDisabled();
12
+ const explicitAliases = options.aliases;
13
+ const loadFileAliases = () => {
14
+ try {
15
+ return registry.loadAliases(options.projectRoot);
16
+ }
17
+ catch {
18
+ return {};
19
+ }
20
+ };
21
+ let liveAliases = { ...loadFileAliases(), ...explicitAliases };
22
+ return {
23
+ /**
24
+ * Re-run discovery and reload cue.toml aliases so elements, tags,
25
+ * and shortcuts added after startup work without a restart.
26
+ * The registry object (and every holder of it) stays valid.
27
+ */
28
+ refresh(disabledElements) {
29
+ if (disabledElements !== undefined)
30
+ options.disabledElements = disabledElements;
31
+ discoverDisabled();
32
+ liveAliases = { ...loadFileAliases(), ...explicitAliases };
33
+ },
34
+ process(input, _promptText) {
35
+ // Expand aliases: /name -> configured Cue string, then re-scan.
36
+ let workingInput = input;
37
+ if (Object.keys(liveAliases).length > 0) {
38
+ const { directives: pre } = scanDirectives(input);
39
+ for (const dir of pre) {
40
+ if (dir.kind === "alias") {
41
+ const expansion = liveAliases[dir.command.command];
42
+ if (expansion) {
43
+ workingInput = workingInput.replace(`/${dir.command.command}`, expansion);
44
+ }
45
+ }
46
+ }
47
+ }
48
+ // Collect marks first: their content blocks must not be scanned as
49
+ // directives, and their content is stripped from the message.
50
+ const marks = collectMarks(workingInput);
51
+ const markContentSpans = marks.map((m) => ({ start: m.contentStart, end: m.contentEnd }));
52
+ const { directives, errors: scanErrors } = scanDirectives(workingInput, markContentSpans);
53
+ const injections = [];
54
+ const harnessActions = [];
55
+ let error;
56
+ // Surface scan errors (malformed directives)
57
+ if (scanErrors.length > 0) {
58
+ error = scanErrors.map((e) => e.message).join("\n");
59
+ }
60
+ // Separate cue directives from system-nav/alias
61
+ const cueDirectives = directives.filter((d) => d.kind === "cue").map((d) => d.directive);
62
+ const nonCueDirectives = directives.filter((d) => d.kind !== "cue");
63
+ // Handle system-nav: route as harness actions and short-circuit.
64
+ // The colon tier terminates entirely in the harness -- the model never sees it.
65
+ for (const dir of nonCueDirectives) {
66
+ if (dir.kind === "system-nav") {
67
+ harnessActions.push({
68
+ element: dir.command.command,
69
+ handler: `harness::${dir.command.command}`,
70
+ args: dir.command.arg ? { arg: dir.command.arg } : {},
71
+ });
72
+ }
73
+ // Alias directives from scanDirectives are already expanded above;
74
+ // any remaining are unresolved and ignored.
75
+ }
76
+ // If system-nav was found, short-circuit: no cue processing, no injections.
77
+ if (harnessActions.length > 0) {
78
+ return { injections: [], harnessActions, error };
79
+ }
80
+ // Duplicate marks are an error; resolve references against the map.
81
+ const marksById = new Map();
82
+ for (const mark of marks) {
83
+ if (marksById.has(mark.id) && error === undefined) {
84
+ error = `Duplicate mark: #${mark.id}`;
85
+ }
86
+ marksById.set(mark.id, mark);
87
+ }
88
+ const markHeaders = new Set(marks.map((m) => m.startIndex));
89
+ // Coalesce cue directives by element (cues are purely behavioral now)
90
+ const groups = coalesceDirectives(cueDirectives);
91
+ // Position-ordered pending injections, interleaving cue behavior
92
+ // with scope statements by their offset in the message.
93
+ const pending = [];
94
+ for (const group of groups) {
95
+ if (group.tags.length > 3) {
96
+ if (error === undefined)
97
+ error = "Maximum 3 tags per directive";
98
+ continue;
99
+ }
100
+ const resolved = resolveDirective({ ...group.directives[0], tags: group.tags }, registry);
101
+ if (!resolved) {
102
+ // Unknown element. Only known built-in harness commands (e.g. Theme)
103
+ // are routed as harness actions; everything else is a real error.
104
+ const KNOWN_HARNESS = new Set(["theme"]);
105
+ if (KNOWN_HARNESS.has(group.element.toLowerCase())) {
106
+ harnessActions.push({
107
+ element: group.element,
108
+ handler: `harness::${group.element.toLowerCase()}`,
109
+ args: { arg: group.tags.join(" ") },
110
+ });
111
+ continue;
112
+ }
113
+ if (error === undefined)
114
+ error = `Element '${group.element}' not defined`;
115
+ continue;
116
+ }
117
+ if (resolved.element.class === "harness") {
118
+ harnessActions.push({
119
+ element: group.element,
120
+ handler: resolved.element.handler ?? "",
121
+ args: {},
122
+ });
123
+ continue;
124
+ }
125
+ for (const text of buildAdditionalContext(resolved)) {
126
+ pending.push({ position: group.position, text });
127
+ }
128
+ }
129
+ // Standalone scope statements (excluding mark headers themselves).
130
+ for (const dir of nonCueDirectives) {
131
+ if (dir.kind !== "scope-only")
132
+ continue;
133
+ const st = dir.statement;
134
+ // Skill scopes ({$name}) are handled by agent-session skill expansion,
135
+ // not cue injection — deliberately never pushed here.
136
+ if (st.scope.type === "id") {
137
+ if (markHeaders.has(st.position))
138
+ continue;
139
+ const mark = marksById.get(st.scope.value);
140
+ if (!mark) {
141
+ if (error === undefined)
142
+ error = `Undefined mark reference: #${st.scope.value}`;
143
+ continue;
144
+ }
145
+ pending.push({ position: st.position, text: `--- #${mark.id} ---\n${mark.content}` });
146
+ }
147
+ else if (st.scope.type !== "skill") {
148
+ pending.push({ position: st.position, text: buildScopeInjection(st.scope, options.projectRoot, marks) });
149
+ }
150
+ }
151
+ // Mark content injections, positioned at their headers.
152
+ for (const mark of marks) {
153
+ pending.push({ position: mark.startIndex, text: `--- #${mark.id} ---\n${mark.content}` });
154
+ }
155
+ pending.sort((a, b) => a.position - b.position);
156
+ // Dedupe identical injections (e.g. a mark injected at its header
157
+ // and at its reference site) by content.
158
+ const seenTexts = new Set();
159
+ for (const p of pending) {
160
+ if (seenTexts.has(p.text))
161
+ continue;
162
+ seenTexts.add(p.text);
163
+ injections.push({ text: p.text, scope: null });
164
+ }
165
+ const result = {
166
+ injections,
167
+ harnessActions,
168
+ error,
169
+ };
170
+ // Strip mark content from the message: it is delivered verbatim
171
+ // via injections, so the model must not see it twice. Cue
172
+ // directives are deliberately preserved: the bracketed text is
173
+ // part of the user's message (e.g. a style suffix like
174
+ // "[Answer: brief > human]") even though its behavior is
175
+ // injected alongside.
176
+ const allStrippedSpans = [...markContentSpans].sort((a, b) => a.start - b.start);
177
+ if (allStrippedSpans.length > 0) {
178
+ result.rewrittenInput = stripSpans(workingInput, allStrippedSpans);
179
+ }
180
+ else {
181
+ // Nothing stripped (cue text is preserved): still report the
182
+ // message the model sees. stripSpans is skipped because it
183
+ // collapses whitespace even with no spans.
184
+ result.rewrittenInput = workingInput;
185
+ }
186
+ return result;
187
+ },
188
+ getRegistry() {
189
+ return registry;
190
+ },
191
+ getOnDebug() {
192
+ return options.onDebug;
193
+ },
194
+ };
195
+ }
196
+ export function additionalContextFromDispatchResult(result) {
197
+ return result.injections.map((inj) => inj.text);
198
+ }
199
+ export function beforeTurn(message, _session, dispatcher) {
200
+ return dispatcher.process(message);
201
+ }
202
+ /**
203
+ * Coalesce duplicate directives.
204
+ * Directives merge only when they name the same element, version, and tag
205
+ * chain; distinct tag chains resolve independently so `[a: x] [a: y]`
206
+ * injects two behavioral contexts instead of pooling tags into one
207
+ * over-capped directive.
208
+ */
209
+ function coalesceDirectives(directives) {
210
+ const groups = new Map();
211
+ for (const dir of directives) {
212
+ const key = `${dir.element.toLowerCase()}::${dir.version ?? ""}::${dir.tags.map((t) => t.toLowerCase()).join(">")}`;
213
+ const existing = groups.get(key);
214
+ if (existing) {
215
+ existing.directives.push(dir);
216
+ existing.position = Math.min(existing.position, dir.position ?? 0);
217
+ }
218
+ else {
219
+ groups.set(key, {
220
+ element: dir.element,
221
+ version: dir.version,
222
+ tags: [...dir.tags],
223
+ directives: [dir],
224
+ position: dir.position ?? 0,
225
+ });
226
+ }
227
+ }
228
+ return Array.from(groups.values());
229
+ }
230
+ //# sourceMappingURL=dispatcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatcher.js","sourceRoot":"","sources":["../src/dispatcher.ts"],"names":[],"mappings":"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","sourcesContent":["import { buildAdditionalContext, buildScopeInjection } from \"./injector.ts\";\nimport { collectMarks } from \"./marks.ts\";\nimport { CueRegistry } from \"./registry.ts\";\nimport { resolveDirective } from \"./resolver.ts\";\nimport { scanDirectives, stripSpans } from \"./scanner.ts\";\nimport type { CueDirective, DispatchResult, HarnessAction, Injection } from \"./types.ts\";\n\nexport interface DispatcherOptions {\n\tprojectRoot?: string;\n\tadditionalRoots?: string[];\n\tskipHome?: boolean;\n\tonDebug?: (message: string) => void;\n\taliases?: Record<string, string>;\n\t/** Lowercased element names to skip at discovery (config UI toggles). */\n\tdisabledElements?: string[];\n}\n\ninterface CoalescedGroup {\n\telement: string;\n\tversion?: string;\n\ttags: string[];\n\tdirectives: CueDirective[];\n\t/** Earliest character offset among the group's directives, for injection ordering. */\n\tposition: number;\n}\n\nexport function cueDispatcher(options: DispatcherOptions = {}) {\n\tconst registry = new CueRegistry(options.additionalRoots, { skipHome: options.skipHome });\n\tconst discoverDisabled = (): void => {\n\t\tregistry.discover(options.projectRoot, { disabled: options.disabledElements });\n\t};\n\tdiscoverDisabled();\n\tconst explicitAliases = options.aliases;\n\tconst loadFileAliases = (): Record<string, string> => {\n\t\ttry {\n\t\t\treturn registry.loadAliases(options.projectRoot);\n\t\t} catch {\n\t\t\treturn {};\n\t\t}\n\t};\n\tlet liveAliases: Record<string, string> = { ...loadFileAliases(), ...explicitAliases };\n\n\treturn {\n\t\t/**\n\t\t * Re-run discovery and reload cue.toml aliases so elements, tags,\n\t\t * and shortcuts added after startup work without a restart.\n\t\t * The registry object (and every holder of it) stays valid.\n\t\t */\n\t\trefresh(disabledElements?: string[]): void {\n\t\t\tif (disabledElements !== undefined) options.disabledElements = disabledElements;\n\t\t\tdiscoverDisabled();\n\t\t\tliveAliases = { ...loadFileAliases(), ...explicitAliases };\n\t\t},\n\t\tprocess(input: string, _promptText?: string): DispatchResult & { rewrittenInput?: string } {\n\t\t\t// Expand aliases: /name -> configured Cue string, then re-scan.\n\t\t\tlet workingInput = input;\n\t\t\tif (Object.keys(liveAliases).length > 0) {\n\t\t\t\tconst { directives: pre } = scanDirectives(input);\n\t\t\t\tfor (const dir of pre) {\n\t\t\t\t\tif (dir.kind === \"alias\") {\n\t\t\t\t\t\tconst expansion = liveAliases[dir.command.command];\n\t\t\t\t\t\tif (expansion) {\n\t\t\t\t\t\t\tworkingInput = workingInput.replace(`/${dir.command.command}`, expansion);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Collect marks first: their content blocks must not be scanned as\n\t\t\t// directives, and their content is stripped from the message.\n\t\t\tconst marks = collectMarks(workingInput);\n\t\t\tconst markContentSpans = marks.map((m) => ({ start: m.contentStart, end: m.contentEnd }));\n\t\t\tconst { directives, errors: scanErrors } = scanDirectives(workingInput, markContentSpans);\n\t\t\tconst injections: Injection[] = [];\n\t\t\tconst harnessActions: HarnessAction[] = [];\n\t\t\tlet error: string | undefined;\n\n\t\t\t// Surface scan errors (malformed directives)\n\t\t\tif (scanErrors.length > 0) {\n\t\t\t\terror = scanErrors.map((e) => e.message).join(\"\\n\");\n\t\t\t}\n\n\t\t\t// Separate cue directives from system-nav/alias\n\t\t\tconst cueDirectives = directives.filter((d) => d.kind === \"cue\").map((d) => d.directive);\n\t\t\tconst nonCueDirectives = directives.filter((d) => d.kind !== \"cue\");\n\n\t\t\t// Handle system-nav: route as harness actions and short-circuit.\n\t\t\t// The colon tier terminates entirely in the harness -- the model never sees it.\n\t\t\tfor (const dir of nonCueDirectives) {\n\t\t\t\tif (dir.kind === \"system-nav\") {\n\t\t\t\t\tharnessActions.push({\n\t\t\t\t\t\telement: dir.command.command,\n\t\t\t\t\t\thandler: `harness::${dir.command.command}`,\n\t\t\t\t\t\targs: dir.command.arg ? { arg: dir.command.arg } : {},\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\t// Alias directives from scanDirectives are already expanded above;\n\t\t\t\t// any remaining are unresolved and ignored.\n\t\t\t}\n\t\t\t// If system-nav was found, short-circuit: no cue processing, no injections.\n\t\t\tif (harnessActions.length > 0) {\n\t\t\t\treturn { injections: [], harnessActions, error };\n\t\t\t}\n\n\t\t\t// Duplicate marks are an error; resolve references against the map.\n\t\t\tconst marksById = new Map<string, (typeof marks)[number]>();\n\t\t\tfor (const mark of marks) {\n\t\t\t\tif (marksById.has(mark.id) && error === undefined) {\n\t\t\t\t\terror = `Duplicate mark: #${mark.id}`;\n\t\t\t\t}\n\t\t\t\tmarksById.set(mark.id, mark);\n\t\t\t}\n\t\t\tconst markHeaders = new Set(marks.map((m) => m.startIndex));\n\n\t\t\t// Coalesce cue directives by element (cues are purely behavioral now)\n\t\t\tconst groups = coalesceDirectives(cueDirectives);\n\n\t\t\t// Position-ordered pending injections, interleaving cue behavior\n\t\t\t// with scope statements by their offset in the message.\n\t\t\tconst pending: Array<{ position: number; text: string }> = [];\n\n\t\t\tfor (const group of groups) {\n\t\t\t\tif (group.tags.length > 3) {\n\t\t\t\t\tif (error === undefined) error = \"Maximum 3 tags per directive\";\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst resolved = resolveDirective({ ...group.directives[0], tags: group.tags }, registry);\n\t\t\t\tif (!resolved) {\n\t\t\t\t\t// Unknown element. Only known built-in harness commands (e.g. Theme)\n\t\t\t\t\t// are routed as harness actions; everything else is a real error.\n\t\t\t\t\tconst KNOWN_HARNESS = new Set([\"theme\"]);\n\t\t\t\t\tif (KNOWN_HARNESS.has(group.element.toLowerCase())) {\n\t\t\t\t\t\tharnessActions.push({\n\t\t\t\t\t\t\telement: group.element,\n\t\t\t\t\t\t\thandler: `harness::${group.element.toLowerCase()}`,\n\t\t\t\t\t\t\targs: { arg: group.tags.join(\" \") },\n\t\t\t\t\t\t});\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif (error === undefined) error = `Element '${group.element}' not defined`;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (resolved.element.class === \"harness\") {\n\t\t\t\t\tharnessActions.push({\n\t\t\t\t\t\telement: group.element,\n\t\t\t\t\t\thandler: resolved.element.handler ?? \"\",\n\t\t\t\t\t\targs: {},\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tfor (const text of buildAdditionalContext(resolved)) {\n\t\t\t\t\tpending.push({ position: group.position, text });\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Standalone scope statements (excluding mark headers themselves).\n\t\t\tfor (const dir of nonCueDirectives) {\n\t\t\t\tif (dir.kind !== \"scope-only\") continue;\n\t\t\t\tconst st = dir.statement;\n\n\t\t\t\t// Skill scopes ({$name}) are handled by agent-session skill expansion,\n\t\t\t\t// not cue injection — deliberately never pushed here.\n\t\t\t\tif (st.scope.type === \"id\") {\n\t\t\t\t\tif (markHeaders.has(st.position)) continue;\n\t\t\t\t\tconst mark = marksById.get(st.scope.value);\n\t\t\t\t\tif (!mark) {\n\t\t\t\t\t\tif (error === undefined) error = `Undefined mark reference: #${st.scope.value}`;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tpending.push({ position: st.position, text: `--- #${mark.id} ---\\n${mark.content}` });\n\t\t\t\t} else if (st.scope.type !== \"skill\") {\n\t\t\t\t\tpending.push({ position: st.position, text: buildScopeInjection(st.scope, options.projectRoot, marks) });\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Mark content injections, positioned at their headers.\n\t\t\tfor (const mark of marks) {\n\t\t\t\tpending.push({ position: mark.startIndex, text: `--- #${mark.id} ---\\n${mark.content}` });\n\t\t\t}\n\n\t\t\tpending.sort((a, b) => a.position - b.position);\n\t\t\t// Dedupe identical injections (e.g. a mark injected at its header\n\t\t\t// and at its reference site) by content.\n\t\t\tconst seenTexts = new Set<string>();\n\t\t\tfor (const p of pending) {\n\t\t\t\tif (seenTexts.has(p.text)) continue;\n\t\t\t\tseenTexts.add(p.text);\n\t\t\t\tinjections.push({ text: p.text, scope: null });\n\t\t\t}\n\n\t\t\tconst result: DispatchResult & { rewrittenInput?: string } = {\n\t\t\t\tinjections,\n\t\t\t\tharnessActions,\n\t\t\t\terror,\n\t\t\t};\n\n\t\t\t// Strip mark content from the message: it is delivered verbatim\n\t\t\t// via injections, so the model must not see it twice. Cue\n\t\t\t// directives are deliberately preserved: the bracketed text is\n\t\t\t// part of the user's message (e.g. a style suffix like\n\t\t\t// \"[Answer: brief > human]\") even though its behavior is\n\t\t\t// injected alongside.\n\t\t\tconst allStrippedSpans = [...markContentSpans].sort((a, b) => a.start - b.start);\n\t\t\tif (allStrippedSpans.length > 0) {\n\t\t\t\tresult.rewrittenInput = stripSpans(workingInput, allStrippedSpans);\n\t\t\t} else {\n\t\t\t\t// Nothing stripped (cue text is preserved): still report the\n\t\t\t\t// message the model sees. stripSpans is skipped because it\n\t\t\t\t// collapses whitespace even with no spans.\n\t\t\t\tresult.rewrittenInput = workingInput;\n\t\t\t}\n\n\t\t\treturn result;\n\t\t},\n\n\t\tgetRegistry() {\n\t\t\treturn registry;\n\t\t},\n\n\t\tgetOnDebug() {\n\t\t\treturn options.onDebug;\n\t\t},\n\t};\n}\n\nexport function additionalContextFromDispatchResult(result: DispatchResult): string[] {\n\treturn result.injections.map((inj) => inj.text);\n}\n\nexport function beforeTurn(\n\tmessage: string,\n\t_session: { mode: string; activeElements: Map<string, unknown>; overrides: Map<string, string> },\n\tdispatcher: ReturnType<typeof cueDispatcher>,\n): DispatchResult {\n\treturn dispatcher.process(message);\n}\n\n/**\n * Coalesce duplicate directives.\n * Directives merge only when they name the same element, version, and tag\n * chain; distinct tag chains resolve independently so `[a: x] [a: y]`\n * injects two behavioral contexts instead of pooling tags into one\n * over-capped directive.\n */\nfunction coalesceDirectives(directives: CueDirective[]): CoalescedGroup[] {\n\tconst groups = new Map<string, CoalescedGroup>();\n\n\tfor (const dir of directives) {\n\t\tconst key = `${dir.element.toLowerCase()}::${dir.version ?? \"\"}::${dir.tags.map((t) => t.toLowerCase()).join(\">\")}`;\n\n\t\tconst existing = groups.get(key);\n\t\tif (existing) {\n\t\t\texisting.directives.push(dir);\n\t\t\texisting.position = Math.min(existing.position, dir.position ?? 0);\n\t\t} else {\n\t\t\tgroups.set(key, {\n\t\t\t\telement: dir.element,\n\t\t\t\tversion: dir.version,\n\t\t\t\ttags: [...dir.tags],\n\t\t\t\tdirectives: [dir],\n\t\t\t\tposition: dir.position ?? 0,\n\t\t\t});\n\t\t}\n\t}\n\n\treturn Array.from(groups.values());\n}\n"]}
@@ -0,0 +1,13 @@
1
+ import type { ElementDef } from "./types.ts";
2
+ export interface ToolDefinition {
3
+ name: string;
4
+ description: string;
5
+ input_schema: {
6
+ type: "object";
7
+ properties: Record<string, unknown>;
8
+ required: string[];
9
+ };
10
+ }
11
+ export declare function generateToolDefinition(element: ElementDef): ToolDefinition | null;
12
+ export declare function registerHarnessTools(elements: ElementDef[]): ToolDefinition[];
13
+ //# sourceMappingURL=harness-bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harness-bridge.d.ts","sourceRoot":"","sources":["../src/harness-bridge.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE;QACb,IAAI,EAAE,QAAQ,CAAC;QACf,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACpC,QAAQ,EAAE,MAAM,EAAE,CAAC;KACnB,CAAC;CACF;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CA2BjF;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,UAAU,EAAE,GAAG,cAAc,EAAE,CAE7E","sourcesContent":["import type { ElementDef } from \"./types.ts\";\n\nexport interface ToolDefinition {\n\tname: string;\n\tdescription: string;\n\tinput_schema: {\n\t\ttype: \"object\";\n\t\tproperties: Record<string, unknown>;\n\t\trequired: string[];\n\t};\n}\n\nexport function generateToolDefinition(element: ElementDef): ToolDefinition | null {\n\tif (element.class !== \"harness\" || !element.handler) {\n\t\treturn null;\n\t}\n\n\tconst properties: Record<string, unknown> = {};\n\tconst required: string[] = [];\n\n\tconst tagNames = Object.keys(element.tags);\n\tif (tagNames.length > 0) {\n\t\tproperties.mode = {\n\t\t\ttype: \"string\",\n\t\t\tdescription: \"The mode to set\",\n\t\t\tenum: tagNames,\n\t\t};\n\t\trequired.push(\"mode\");\n\t}\n\n\treturn {\n\t\tname: element.handler.replace(/^harness::/, \"\"),\n\t\tdescription: element.description,\n\t\tinput_schema: {\n\t\t\ttype: \"object\",\n\t\t\tproperties,\n\t\t\trequired,\n\t\t},\n\t};\n}\n\nexport function registerHarnessTools(elements: ElementDef[]): ToolDefinition[] {\n\treturn elements.map(generateToolDefinition).filter((t): t is ToolDefinition => t !== null);\n}\n"]}
@@ -0,0 +1,29 @@
1
+ export function generateToolDefinition(element) {
2
+ if (element.class !== "harness" || !element.handler) {
3
+ return null;
4
+ }
5
+ const properties = {};
6
+ const required = [];
7
+ const tagNames = Object.keys(element.tags);
8
+ if (tagNames.length > 0) {
9
+ properties.mode = {
10
+ type: "string",
11
+ description: "The mode to set",
12
+ enum: tagNames,
13
+ };
14
+ required.push("mode");
15
+ }
16
+ return {
17
+ name: element.handler.replace(/^harness::/, ""),
18
+ description: element.description,
19
+ input_schema: {
20
+ type: "object",
21
+ properties,
22
+ required,
23
+ },
24
+ };
25
+ }
26
+ export function registerHarnessTools(elements) {
27
+ return elements.map(generateToolDefinition).filter((t) => t !== null);
28
+ }
29
+ //# sourceMappingURL=harness-bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harness-bridge.js","sourceRoot":"","sources":["../src/harness-bridge.ts"],"names":[],"mappings":"AAYA,MAAM,UAAU,sBAAsB,CAAC,OAAmB,EAAyB;IAClF,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QACrD,OAAO,IAAI,CAAC;IACb,CAAC;IAED,MAAM,UAAU,GAA4B,EAAE,CAAC;IAC/C,MAAM,QAAQ,GAAa,EAAE,CAAC;IAE9B,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACzB,UAAU,CAAC,IAAI,GAAG;YACjB,IAAI,EAAE,QAAQ;YACd,WAAW,EAAE,iBAAiB;YAC9B,IAAI,EAAE,QAAQ;SACd,CAAC;QACF,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACvB,CAAC;IAED,OAAO;QACN,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC;QAC/C,WAAW,EAAE,OAAO,CAAC,WAAW;QAChC,YAAY,EAAE;YACb,IAAI,EAAE,QAAQ;YACd,UAAU;YACV,QAAQ;SACR;KACD,CAAC;AAAA,CACF;AAED,MAAM,UAAU,oBAAoB,CAAC,QAAsB,EAAoB;IAC9E,OAAO,QAAQ,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAuB,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;AAAA,CAC3F","sourcesContent":["import type { ElementDef } from \"./types.ts\";\n\nexport interface ToolDefinition {\n\tname: string;\n\tdescription: string;\n\tinput_schema: {\n\t\ttype: \"object\";\n\t\tproperties: Record<string, unknown>;\n\t\trequired: string[];\n\t};\n}\n\nexport function generateToolDefinition(element: ElementDef): ToolDefinition | null {\n\tif (element.class !== \"harness\" || !element.handler) {\n\t\treturn null;\n\t}\n\n\tconst properties: Record<string, unknown> = {};\n\tconst required: string[] = [];\n\n\tconst tagNames = Object.keys(element.tags);\n\tif (tagNames.length > 0) {\n\t\tproperties.mode = {\n\t\t\ttype: \"string\",\n\t\t\tdescription: \"The mode to set\",\n\t\t\tenum: tagNames,\n\t\t};\n\t\trequired.push(\"mode\");\n\t}\n\n\treturn {\n\t\tname: element.handler.replace(/^harness::/, \"\"),\n\t\tdescription: element.description,\n\t\tinput_schema: {\n\t\t\ttype: \"object\",\n\t\t\tproperties,\n\t\t\trequired,\n\t\t},\n\t};\n}\n\nexport function registerHarnessTools(elements: ElementDef[]): ToolDefinition[] {\n\treturn elements.map(generateToolDefinition).filter((t): t is ToolDefinition => t !== null);\n}\n"]}
@@ -0,0 +1,12 @@
1
+ import type { CueDirective, CueRegisteredTool, ElementDef, HarnessHandler, HarnessResult, SessionState } from "./types.ts";
2
+ export declare class HarnessRouter {
3
+ private handlers;
4
+ register(name: string, handler: HarnessHandler): void;
5
+ route(directive: CueDirective, element: ElementDef, session: SessionState): Promise<HarnessResult>;
6
+ routeColonTier(message: string, session: SessionState): Promise<HarnessResult[]>;
7
+ generateToolSchema(element: ElementDef): CueRegisteredTool | null;
8
+ getToolSchemas(): CueRegisteredTool[];
9
+ private extractArgs;
10
+ private parseColonCommands;
11
+ }
12
+ //# sourceMappingURL=harness-router.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harness-router.d.ts","sourceRoot":"","sources":["../src/harness-router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,YAAY,EACZ,iBAAiB,EACjB,UAAU,EAEV,cAAc,EACd,aAAa,EACb,YAAY,EACZ,MAAM,YAAY,CAAC;AAEpB,qBAAa,aAAa;IACzB,OAAO,CAAC,QAAQ,CAAqC;IAErD,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAEpD;IAEK,KAAK,CAAC,SAAS,EAAE,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC,CAuBvG;IAEK,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAgDrF;IAED,kBAAkB,CAAC,OAAO,EAAE,UAAU,GAAG,iBAAiB,GAAG,IAAI,CAsChE;IAED,cAAc,IAAI,iBAAiB,EAAE,CAqCpC;IAED,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,kBAAkB;CAsB1B","sourcesContent":["import type {\n\tCueDirective,\n\tCueRegisteredTool,\n\tElementDef,\n\tHarnessContext,\n\tHarnessHandler,\n\tHarnessResult,\n\tSessionState,\n} from \"./types.ts\";\n\nexport class HarnessRouter {\n\tprivate handlers = new Map<string, HarnessHandler>();\n\n\tregister(name: string, handler: HarnessHandler): void {\n\t\tthis.handlers.set(name, handler);\n\t}\n\n\tasync route(directive: CueDirective, element: ElementDef, session: SessionState): Promise<HarnessResult> {\n\t\tconst handlerName = element.handler;\n\t\tif (!handlerName) {\n\t\t\treturn { action: \"error\", error: `Element ${element.name} has no handler` };\n\t\t}\n\t\tconst handler = this.handlers.get(handlerName);\n\t\tif (!handler) {\n\t\t\treturn { action: \"error\", error: `Unknown handler: ${handlerName}` };\n\t\t}\n\n\t\tconst args = this.extractArgs(directive);\n\t\tconst ctx: HarnessContext = {\n\t\t\telement,\n\t\t\tdirective,\n\t\t\tsession,\n\t\t\targs,\n\t\t};\n\n\t\ttry {\n\t\t\treturn await handler(ctx);\n\t\t} catch (e) {\n\t\t\treturn { action: \"error\", error: String(e) };\n\t\t}\n\t}\n\n\tasync routeColonTier(message: string, session: SessionState): Promise<HarnessResult[]> {\n\t\tconst results: HarnessResult[] = [];\n\t\tconst commands = this.parseColonCommands(message);\n\t\tfor (const cmd of commands) {\n\t\t\t// For now, we need to resolve the element from the command name.\n\t\t\t// This will be integrated with the registry later.\n\t\t\t// For simplicity, we assume the command name maps to a handler name \"harness::<cmd>\".\n\t\t\tconst handlerName = `harness::${cmd.command}`;\n\t\t\tconst handler = this.handlers.get(handlerName);\n\t\t\tif (!handler) {\n\t\t\t\tresults.push({ action: \"error\", error: `Unknown handler: ${handlerName}` });\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t// We need a dummy element and directive for context.\n\t\t\t// This will be properly implemented when integrated with dispatcher.\n\t\t\tconst dummyElement: ElementDef = {\n\t\t\t\tname: cmd.command,\n\t\t\t\tdescription: \"\",\n\t\t\t\tversion: \"\",\n\t\t\t\tclass: \"harness\",\n\t\t\t\thandler: handlerName,\n\t\t\t\ttags: {},\n\t\t\t\tuses: [],\n\t\t\t\tbodyPath: \"\",\n\t\t\t\ttomlPath: \"\",\n\t\t\t};\n\t\t\tconst dummyDirective: CueDirective = {\n\t\t\t\traw: cmd.raw,\n\t\t\t\telement: cmd.command,\n\t\t\t\ttags: [],\n\t\t\t\tscope: null,\n\t\t\t\tline: 0,\n\t\t\t\tcol: 0,\n\t\t\t};\n\t\t\tconst ctx: HarnessContext = {\n\t\t\t\telement: dummyElement,\n\t\t\t\tdirective: dummyDirective,\n\t\t\t\tsession,\n\t\t\t\targs: cmd.arg ? { arg: cmd.arg } : {},\n\t\t\t};\n\t\t\ttry {\n\t\t\t\tconst result = await handler(ctx);\n\t\t\t\tresults.push(result);\n\t\t\t} catch (e) {\n\t\t\t\tresults.push({ action: \"error\", error: String(e) });\n\t\t\t}\n\t\t}\n\t\treturn results;\n\t}\n\n\tgenerateToolSchema(element: ElementDef): CueRegisteredTool | null {\n\t\tif (element.class !== \"harness\" || !element.handler) {\n\t\t\treturn null;\n\t\t}\n\t\tconst inputSchema: Record<string, unknown> = {\n\t\t\ttype: \"object\",\n\t\t\tproperties: {},\n\t\t\trequired: [],\n\t\t};\n\t\tif (element.inputs) {\n\t\t\tconst properties = inputSchema.properties as Record<string, unknown>;\n\t\t\tconst required = inputSchema.required as string[];\n\t\t\tfor (const input of element.inputs) {\n\t\t\t\tproperties[input] = { type: \"string\" };\n\t\t\t\trequired.push(input);\n\t\t\t}\n\t\t}\n\t\treturn {\n\t\t\tname: element.name,\n\t\t\tdescription: element.description,\n\t\t\tinputSchema,\n\t\t\texecute: async (args: Record<string, unknown>) => {\n\t\t\t\tconst directive: CueDirective = {\n\t\t\t\t\traw: `:${element.name} ${Object.values(args).join(\" \")}`,\n\t\t\t\t\telement: element.name,\n\t\t\t\t\ttags: [],\n\t\t\t\t\tscope: null,\n\t\t\t\t\tline: 0,\n\t\t\t\t\tcol: 0,\n\t\t\t\t};\n\t\t\t\treturn await this.route(directive, element, {\n\t\t\t\t\tmode: \"default\",\n\t\t\t\t\tactiveElements: new Map(),\n\t\t\t\t\toverrides: new Map(),\n\t\t\t\t\tdispatchHistory: [],\n\t\t\t\t});\n\t\t\t},\n\t\t};\n\t}\n\n\tgetToolSchemas(): CueRegisteredTool[] {\n\t\treturn Array.from(this.handlers.entries()).map(([handlerName]) => {\n\t\t\t// Derive element-like metadata from handler name\n\t\t\tconst name = handlerName.split(\"::\").pop() ?? handlerName;\n\t\t\treturn {\n\t\t\t\tname,\n\t\t\t\tdescription: `Harness handler: ${handlerName}`,\n\t\t\t\tinputSchema: { type: \"object\", properties: {}, required: [] },\n\t\t\t\texecute: async (args: Record<string, unknown>) => {\n\t\t\t\t\tconst directive: CueDirective = {\n\t\t\t\t\t\traw: `:${name} ${Object.values(args).join(\" \")}`,\n\t\t\t\t\t\telement: name,\n\t\t\t\t\t\ttags: [],\n\t\t\t\t\t\tscope: null,\n\t\t\t\t\t\tline: 0,\n\t\t\t\t\t\tcol: 0,\n\t\t\t\t\t};\n\t\t\t\t\tconst element: ElementDef = {\n\t\t\t\t\t\tname,\n\t\t\t\t\t\tdescription: \"\",\n\t\t\t\t\t\tversion: \"\",\n\t\t\t\t\t\tclass: \"harness\",\n\t\t\t\t\t\thandler: handlerName,\n\t\t\t\t\t\ttags: {},\n\t\t\t\t\t\tuses: [],\n\t\t\t\t\t\tbodyPath: \"\",\n\t\t\t\t\t\ttomlPath: \"\",\n\t\t\t\t\t};\n\t\t\t\t\treturn await this.route(directive, element, {\n\t\t\t\t\t\tmode: \"default\",\n\t\t\t\t\t\tactiveElements: new Map(),\n\t\t\t\t\t\toverrides: new Map(),\n\t\t\t\t\t\tdispatchHistory: [],\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t};\n\t\t});\n\t}\n\n\tprivate extractArgs(directive: CueDirective): Record<string, unknown> {\n\t\tconst args: Record<string, unknown> = {};\n\t\tif (directive.tags.length > 0) {\n\t\t\targs.tags = directive.tags;\n\t\t\targs.color = directive.tags[0];\n\t\t\targs.arg = directive.tags.join(\" \");\n\t\t}\n\t\treturn args;\n\t}\n\n\tprivate parseColonCommands(message: string): Array<{ command: string; arg?: string; raw: string }> {\n\t\tconst commands: Array<{ command: string; arg?: string; raw: string }> = [];\n\t\t// Simple parsing: split by semicolon, trim, find colon at start.\n\t\tconst parts = message.split(\";\");\n\t\tfor (const part of parts) {\n\t\t\tconst trimmed = part.trim();\n\t\t\tif (trimmed.startsWith(\":\")) {\n\t\t\t\tconst spaceIdx = trimmed.indexOf(\" \");\n\t\t\t\tif (spaceIdx === -1) {\n\t\t\t\t\tcommands.push({\n\t\t\t\t\t\tcommand: trimmed.slice(1),\n\t\t\t\t\t\traw: trimmed,\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\tconst command = trimmed.slice(1, spaceIdx);\n\t\t\t\t\tconst arg = trimmed.slice(spaceIdx + 1).trim();\n\t\t\t\t\tcommands.push({ command, arg, raw: trimmed });\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn commands;\n\t}\n}\n"]}