@litfamily/litopencode 1.0.28 → 1.0.30

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 (156) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README-Ko-KR.md +7 -7
  3. package/README.md +7 -7
  4. package/dist/activation-primary-prompts.js +15 -6
  5. package/dist/activation-prompt-utils.d.ts +2 -2
  6. package/dist/activation-prompt-utils.js +9 -3
  7. package/dist/activation-reentry.d.ts +29 -0
  8. package/dist/activation-reentry.js +71 -0
  9. package/dist/activation-routing.d.ts +1 -1
  10. package/dist/activation-routing.js +40 -7
  11. package/dist/activation-workflow-prompts.d.ts +2 -0
  12. package/dist/activation-workflow-prompts.js +32 -7
  13. package/dist/activation.d.ts +6 -1
  14. package/dist/activation.js +34 -7
  15. package/dist/cli/args.js +14 -0
  16. package/dist/cli/auto-update.js +2 -4
  17. package/dist/cli/command-plan.d.ts +40 -0
  18. package/dist/cli/command-plan.js +62 -0
  19. package/dist/cli/doctor.d.ts +4 -1
  20. package/dist/cli/doctor.js +65 -4
  21. package/dist/cli/host-detect.d.ts +58 -0
  22. package/dist/cli/host-detect.js +199 -0
  23. package/dist/cli/install-report.js +3 -1
  24. package/dist/cli/install.d.ts +9 -1
  25. package/dist/cli/install.js +134 -31
  26. package/dist/cli/managed-skill-assets.d.ts +1 -1
  27. package/dist/cli/managed-skill-assets.js +5 -3
  28. package/dist/cli/office-dependencies.d.ts +2 -0
  29. package/dist/cli/office-dependencies.js +7 -3
  30. package/dist/cli/scientific-visualization-dependencies.js +1 -1
  31. package/dist/cli/tui-config.d.ts +27 -2
  32. package/dist/cli/tui-config.js +77 -2
  33. package/dist/cli/types.d.ts +17 -0
  34. package/dist/cli.js +9 -6
  35. package/dist/commands.d.ts +10 -1
  36. package/dist/commands.js +17 -3
  37. package/dist/deliverable-hedge-guard.js +21 -9
  38. package/dist/features.d.ts +27 -1
  39. package/dist/features.js +40 -8
  40. package/dist/host/server.d.ts +7 -0
  41. package/dist/host/server.js +12 -0
  42. package/dist/host/tui.d.ts +7 -0
  43. package/dist/host/tui.js +12 -0
  44. package/dist/server.js +11 -1
  45. package/dist/skill-renames.d.ts +1 -1
  46. package/dist/skills.d.ts +9 -2
  47. package/dist/skills.js +22 -8
  48. package/dist/tui/mission-source-v2.d.ts +51 -0
  49. package/dist/tui/mission-source-v2.js +52 -0
  50. package/dist/v2/agents.d.ts +105 -0
  51. package/dist/v2/agents.js +185 -0
  52. package/dist/v2/commands.d.ts +44 -0
  53. package/dist/v2/commands.js +65 -0
  54. package/dist/v2/context.d.ts +67 -0
  55. package/dist/v2/context.js +17 -0
  56. package/dist/v2/events.d.ts +55 -0
  57. package/dist/v2/events.js +193 -0
  58. package/dist/v2/hooks.d.ts +39 -0
  59. package/dist/v2/hooks.js +61 -0
  60. package/dist/v2/server.d.ts +2 -0
  61. package/dist/v2/server.js +68 -0
  62. package/dist/v2/session-hooks.d.ts +64 -0
  63. package/dist/v2/session-hooks.js +230 -0
  64. package/dist/v2/tools.d.ts +26 -0
  65. package/dist/v2/tools.js +49 -0
  66. package/dist/v2/tui.d.ts +63 -0
  67. package/dist/v2/tui.js +259 -0
  68. package/docs/assets/cover-motion-still.webp +0 -0
  69. package/docs/assets/cover-motion.webp +0 -0
  70. package/docs/assets/cover.webp +0 -0
  71. package/docs/assets/readme/badge-version.svg +1 -1
  72. package/docs/assets/readme/litfamily-machines.png +0 -0
  73. package/docs/migration.md +4 -3
  74. package/docs/reference-Ko-KR.md +109 -7
  75. package/docs/reference.md +122 -9
  76. package/package.json +10 -5
  77. package/rules/bundled-rules/lit-humanizer.md +2 -0
  78. package/skills/deep-interview/SKILL.md +1 -1
  79. package/skills/lit-brainstorm/SKILL.md +299 -0
  80. package/skills/lit-brainstorm/references/handoff-briefs.md +72 -0
  81. package/skills/lit-brainstorm/references/lenses.md +106 -0
  82. package/skills/lit-brainstorm/references/scoring-and-convergence.md +93 -0
  83. package/skills/lit-brainstorm/references/session-notes-and-lanes.md +122 -0
  84. package/skills/lit-docx/SKILL.md +1 -1
  85. package/skills/lit-docx/run.py +42 -4
  86. package/skills/lit-docx/scripts/convert_md_to_pdf.py +38 -0
  87. package/skills/lit-docx/templates/latex/acs/template.tex +2 -0
  88. package/skills/lit-docx/templates/latex/elsevier/template.tex +2 -0
  89. package/skills/lit-docx/templates/latex/ieee/template.tex +2 -0
  90. package/skills/lit-docx/templates/latex/korean-generic/template.tex +2 -0
  91. package/skills/lit-docx/templates/latex/nature/template.tex +2 -0
  92. package/skills/lit-humanizer/SKILL.md +4 -0
  93. package/skills/lit-humanizer/scripts/core.mjs +81 -1
  94. package/skills/lit-humanizer/scripts/detect.mjs +4 -3
  95. package/skills/lit-humanizer/scripts/guard-scan.mjs +12 -6
  96. package/skills/lit-pptx/SKILL.md +1 -1
  97. package/skills/lit-pptx/command.mjs +94 -0
  98. package/skills/lit-pptx/run.mjs +16 -6
  99. package/skills/lit-pptx/scripts/compile-deck.js +2 -2
  100. package/skills/lit-typographic-motion/SKILL.md +5 -2
  101. package/skills/lit-typographic-motion/THIRD_PARTY_NOTICES +3 -0
  102. package/skills/lit-typographic-motion/engine/browser.mjs +3 -9
  103. package/skills/lit-typographic-motion/engine/command.mjs +94 -0
  104. package/skills/lit-typographic-motion/engine/constants.mjs +29 -1
  105. package/skills/lit-typographic-motion/engine/cover/assets.mjs +91 -0
  106. package/skills/lit-typographic-motion/engine/cover/capture.mjs +173 -0
  107. package/skills/lit-typographic-motion/engine/cover/decode.mjs +173 -0
  108. package/skills/lit-typographic-motion/engine/cover/frame-worker.mjs +19 -0
  109. package/skills/lit-typographic-motion/engine/cover/gate.mjs +261 -0
  110. package/skills/lit-typographic-motion/engine/cover/image.mjs +138 -0
  111. package/skills/lit-typographic-motion/engine/cover/render.mjs +323 -0
  112. package/skills/lit-typographic-motion/engine/cover/sheet.mjs +65 -0
  113. package/skills/lit-typographic-motion/engine/cover/treatment.mjs +110 -0
  114. package/skills/lit-typographic-motion/engine/encode.mjs +1 -1
  115. package/skills/lit-typographic-motion/engine/look.mjs +113 -10
  116. package/skills/lit-typographic-motion/engine/sound.mjs +1 -1
  117. package/skills/lit-typographic-motion/engine/stage/capture.mjs +26 -18
  118. package/skills/lit-typographic-motion/engine/stage/cover-init.js +38 -0
  119. package/skills/lit-typographic-motion/engine/stage/loop-kit.js +102 -0
  120. package/skills/lit-typographic-motion/engine/stage/render.mjs +4 -4
  121. package/skills/lit-typographic-motion/engine/treatment.mjs +1 -0
  122. package/skills/lit-typographic-motion/fonts/JetBrainsMono-OFL.txt +93 -0
  123. package/skills/lit-typographic-motion/fonts/JetBrainsMono-wght-latin.woff2 +0 -0
  124. package/skills/lit-typographic-motion/fonts/pins.json +3 -1
  125. package/skills/lit-typographic-motion/gate.mjs +34 -1
  126. package/skills/lit-typographic-motion/probe.mjs +8 -3
  127. package/skills/lit-typographic-motion/recipes/negatives/card-scenes/expected.json +16 -0
  128. package/skills/lit-typographic-motion/recipes/negatives/card-scenes/index.html +35 -0
  129. package/skills/lit-typographic-motion/recipes/negatives/reset-sweep/expected.json +16 -0
  130. package/skills/lit-typographic-motion/recipes/negatives/reset-sweep/index.html +29 -0
  131. package/skills/lit-typographic-motion/recipes/negatives/still-glow/expected.json +17 -0
  132. package/skills/lit-typographic-motion/recipes/negatives/still-glow/index.html +29 -0
  133. package/skills/lit-typographic-motion/recipes/negatives/title-slam/expected.json +17 -0
  134. package/skills/lit-typographic-motion/recipes/negatives/title-slam/index.html +35 -0
  135. package/skills/lit-typographic-motion/recipes/orbit-loop/expected.json +62 -0
  136. package/skills/lit-typographic-motion/recipes/orbit-loop/index.html +100 -0
  137. package/skills/lit-typographic-motion/recipes/painted-light/expected.json +95 -0
  138. package/skills/lit-typographic-motion/recipes/painted-light/index.html +232 -0
  139. package/skills/lit-typographic-motion/recipes/weight-tide/expected.json +62 -0
  140. package/skills/lit-typographic-motion/recipes/weight-tide/index.html +81 -0
  141. package/skills/lit-typographic-motion/references/cover.md +423 -0
  142. package/skills/lit-typographic-motion/references/craft-loop.md +2 -0
  143. package/skills/lit-typographic-motion/references/runtime.md +17 -2
  144. package/skills/lit-typographic-motion/references/stage.md +3 -0
  145. package/skills/lit-typographic-motion/references/treatment.md +4 -0
  146. package/skills/lit-typographic-motion/render.mjs +30 -6
  147. package/skills/lit-typographic-motion/runtime.mjs +12 -13
  148. package/skills/{durable-litgoal → litgoal}/SKILL.md +31 -12
  149. package/skills/managed-skill-manifest.json +175 -31
  150. package/skills/review-work/SKILL.md +27 -18
  151. package/skills/skill-rename-aliases.json +2 -1
  152. package/tools/check-pack-payload.mjs +4 -1
  153. package/tools/payload-substance-allowlist.json +1 -1
  154. package/tools/payload-substance-parity.json +6 -4
  155. package/tools/qa-cover-recipes.mjs +167 -0
  156. package/tools/version-manifests.json +6 -0
@@ -0,0 +1,105 @@
1
+ import type { LitOpenCodeConfig } from "../config.ts";
2
+ export type PermissionEffect = "allow" | "deny" | "ask";
3
+ export type PermissionRule = {
4
+ action: string;
5
+ resource: string;
6
+ effect: PermissionEffect;
7
+ };
8
+ export type AgentModelRef = {
9
+ providerID: string;
10
+ id: string;
11
+ variant?: string;
12
+ };
13
+ /** The writable slice of an OpenCode 2 agent that LitOpenCode sets. */
14
+ export type EditableAgent = {
15
+ system?: string;
16
+ description?: string;
17
+ mode: "subagent" | "primary" | "all";
18
+ hidden: boolean;
19
+ color?: string;
20
+ steps?: number;
21
+ model?: AgentModelRef;
22
+ permissions: PermissionRule[];
23
+ };
24
+ export type AgentEditorLike = {
25
+ default(id: string | undefined): void;
26
+ update(id: string, update: (agent: any) => void): void;
27
+ };
28
+ export type ModelEditorLike = {
29
+ get(providerID: string, modelID: string): unknown;
30
+ update(providerID: string, modelID: string, update: (model: any) => void): void;
31
+ };
32
+ /** Request settings OpenCode 2 keeps on an agent but does not send yet; the context hook sends them. */
33
+ export type AgentRequestOptions = Readonly<Record<string, unknown>>;
34
+ export type LitAgentPlan = {
35
+ readonly agents: ReadonlyMap<string, EditableAgent>;
36
+ readonly requestOptions: ReadonlyMap<string, AgentRequestOptions>;
37
+ readonly hostAgents: readonly string[];
38
+ };
39
+ /**
40
+ * Agents that only plan: the host rules deny them edits, the shell, subagents and Code Mode, and the
41
+ * evaluate hook enforces it again. Code Mode (`execute`) runs model-written JavaScript with `fetch` and
42
+ * the catalog tools, which OpenCode 1 never offered; a tool whose last matching rule denies "*" is left
43
+ * out of the request entirely.
44
+ */
45
+ export declare const planningOnlyAgents: ReadonlySet<string>;
46
+ /** OpenCode 2 renamed these tools; a migrated rule must use the new name to keep matching. */
47
+ export declare function normalizeAction(action: string): string;
48
+ /**
49
+ * Turns an OpenCode 1 permission map into ordered rules, the way OpenCode 2 migrates config files:
50
+ * tool switches first (true allows, false denies), then permission keys, a later key replacing an
51
+ * earlier one in place, and a pattern map becoming one rule per pattern in its written order.
52
+ */
53
+ export declare function permissionRules(tools: Readonly<Record<string, boolean>> | undefined, permission: unknown): PermissionRule[];
54
+ /** Builds every agent edit from the LitOpenCode config, without touching the host. */
55
+ export declare function planLitAgents(config: LitOpenCodeConfig): LitAgentPlan;
56
+ /** Applies a plan inside ctx.agent.transform: creates or updates each agent, hides the host agents, sets the default. */
57
+ export declare function applyLitAgents(editor: AgentEditorLike, plan: LitAgentPlan): void;
58
+ /**
59
+ * The OpenAI rows LitOpenCode routes to, added to the host catalog when it lacks one, as the OpenCode 1
60
+ * `config` hook does. A row the host already has is left as the host made it, and the editor cannot
61
+ * create a provider the user has not set up. Each effort becomes a variant shaped like the host's own
62
+ * OpenAI rows, so a route such as `#xhigh` resolves.
63
+ */
64
+ export declare function registerOpenAiProviderModels(editor: ModelEditorLike): void;
65
+ export type SessionModelInfoLike = {
66
+ readonly parentID?: string;
67
+ readonly agent?: string;
68
+ readonly model?: unknown;
69
+ };
70
+ export type SessionModelDomainLike = {
71
+ readonly get: (input: {
72
+ readonly sessionID: string;
73
+ }) => Promise<SessionModelInfoLike>;
74
+ readonly switchModel: (input: {
75
+ readonly sessionID: string;
76
+ readonly model: AgentModelRef;
77
+ }) => Promise<unknown>;
78
+ };
79
+ type CatalogModelLike = {
80
+ readonly providerID: string;
81
+ readonly id: string;
82
+ readonly enabled?: boolean;
83
+ };
84
+ export type ModelListLike = {
85
+ readonly list: () => Promise<readonly CatalogModelLike[] | {
86
+ readonly data: readonly CatalogModelLike[];
87
+ }>;
88
+ };
89
+ /**
90
+ * OpenCode 2 runs a session that has no model of its own on the host default model: the agent's model
91
+ * is only read by a client that creates the session (the TUI does, `opencode2 run` does not). This sets
92
+ * such a root session to its agent's route, lit-loop when it names none, once the route is in the
93
+ * catalog. A session with a model, a delegated child (the host gives it its agent's model) and an agent
94
+ * LitOpenCode does not own keep the host's choice.
95
+ */
96
+ export declare function createSessionModelRoute(plan: LitAgentPlan, session: SessionModelDomainLike, models: ModelListLike): (sessionID: string) => Promise<void>;
97
+ export type PermissionEvaluationLike = {
98
+ readonly agent?: string;
99
+ readonly action: string;
100
+ effect: PermissionEffect;
101
+ message?: string;
102
+ };
103
+ /** The second lock for planning-only agents: whatever the rules say, edits, the shell, subagents and Code Mode are denied. */
104
+ export declare function enforcePlanningOnly(evaluation: PermissionEvaluationLike): void;
105
+ export {};
@@ -0,0 +1,185 @@
1
+ // LitOpenCode's agents on OpenCode 2. The roster, prompts, routes and permission modes are built by the
2
+ // same code the OpenCode 1 `config` hook uses; this module only re-expresses the result in OpenCode 2
3
+ // terms: an ordered permission ruleset instead of tool switches and a permission map, `system` instead
4
+ // of `prompt`, `steps` instead of `maxSteps`, and a model reference instead of a model string.
5
+ import { litOpenCodeAgents, registerLitOpenCodeAgents } from "../agents.js";
6
+ import { openAiModelEfforts, openaiProviderModels } from "../cli/model-catalog.js";
7
+ /**
8
+ * Agents that only plan: the host rules deny them edits, the shell, subagents and Code Mode, and the
9
+ * evaluate hook enforces it again. Code Mode (`execute`) runs model-written JavaScript with `fetch` and
10
+ * the catalog tools, which OpenCode 1 never offered; a tool whose last matching rule denies "*" is left
11
+ * out of the request entirely.
12
+ */
13
+ export const planningOnlyAgents = new Set(["lit-plan"]);
14
+ const planningDeniedActions = ["edit", "shell", "subagent", "execute"];
15
+ const planningDeniedAliases = new Set([...planningDeniedActions, "write", "patch", "bash", "task"]);
16
+ /** OpenCode 2 renamed these tools; a migrated rule must use the new name to keep matching. */
17
+ export function normalizeAction(action) {
18
+ if (action === "write" || action === "patch")
19
+ return "edit";
20
+ if (action === "task")
21
+ return "subagent";
22
+ if (action === "bash")
23
+ return "shell";
24
+ return action;
25
+ }
26
+ function isEffect(value) {
27
+ return value === "allow" || value === "deny" || value === "ask";
28
+ }
29
+ /**
30
+ * Turns an OpenCode 1 permission map into ordered rules, the way OpenCode 2 migrates config files:
31
+ * tool switches first (true allows, false denies), then permission keys, a later key replacing an
32
+ * earlier one in place, and a pattern map becoming one rule per pattern in its written order.
33
+ */
34
+ export function permissionRules(tools, permission) {
35
+ const entries = new Map();
36
+ for (const [tool, enabled] of Object.entries(tools ?? {}))
37
+ entries.set(normalizeAction(tool), enabled ? "allow" : "deny");
38
+ if (isEffect(permission))
39
+ entries.set("*", permission);
40
+ else if (typeof permission === "object" && permission !== null && !Array.isArray(permission)) {
41
+ for (const [key, value] of Object.entries(permission))
42
+ entries.set(normalizeAction(key), value);
43
+ }
44
+ const rules = [];
45
+ for (const [action, value] of entries) {
46
+ if (isEffect(value)) {
47
+ rules.push({ action, resource: "*", effect: value });
48
+ continue;
49
+ }
50
+ if (typeof value !== "object" || value === null || Array.isArray(value))
51
+ continue;
52
+ for (const [resource, effect] of Object.entries(value))
53
+ if (isEffect(effect))
54
+ rules.push({ action, resource, effect });
55
+ }
56
+ return rules;
57
+ }
58
+ function modelRef(model, variant) {
59
+ if (model === undefined)
60
+ return undefined;
61
+ const slash = model.indexOf("/");
62
+ if (slash <= 0 || slash === model.length - 1)
63
+ return undefined;
64
+ return {
65
+ providerID: model.slice(0, slash),
66
+ id: model.slice(slash + 1),
67
+ ...(variant === undefined || variant === "" || variant.includes("#") ? {} : { variant })
68
+ };
69
+ }
70
+ function requestOptions(agent) {
71
+ return {
72
+ ...(agent.temperature === undefined ? {} : { temperature: agent.temperature }),
73
+ ...(agent.top_p === undefined ? {} : { topP: agent.top_p }),
74
+ ...(agent.maxTokens === undefined ? {} : { maxTokens: agent.maxTokens }),
75
+ ...(agent.reasoningEffort === undefined ? {} : { reasoningEffort: agent.reasoningEffort }),
76
+ ...(agent.textVerbosity === undefined ? {} : { textVerbosity: agent.textVerbosity })
77
+ };
78
+ }
79
+ /** Builds every agent edit from the LitOpenCode config, without touching the host. */
80
+ export function planLitAgents(config) {
81
+ const target = {};
82
+ registerLitOpenCodeAgents(target, config);
83
+ // Balanced and yolo modes widen the global permission on OpenCode 1. A plugin cannot edit the global
84
+ // ruleset on OpenCode 2, so the same rules lead each LitOpenCode agent's own ruleset instead.
85
+ const globalRules = permissionRules(undefined, target.permission);
86
+ const agents = new Map();
87
+ const options = new Map();
88
+ for (const definition of litOpenCodeAgents) {
89
+ const v1 = target.agent?.[definition.id];
90
+ if (v1 === undefined)
91
+ continue;
92
+ const rules = [...globalRules, ...permissionRules(v1.tools, v1.permission)];
93
+ if (planningOnlyAgents.has(definition.id)) {
94
+ for (const action of planningDeniedActions)
95
+ rules.push({ action, resource: "*", effect: "deny" });
96
+ }
97
+ const model = modelRef(v1.model, v1.variant);
98
+ agents.set(definition.id, {
99
+ ...(v1.prompt === undefined ? {} : { system: v1.prompt }),
100
+ ...(v1.description === undefined ? {} : { description: v1.description }),
101
+ mode: v1.mode === "subagent" ? "subagent" : v1.mode === "primary" ? "primary" : "all",
102
+ hidden: v1.hidden === true,
103
+ ...(typeof v1.color === "string" && v1.color.startsWith("#") ? { color: v1.color } : {}),
104
+ ...(typeof v1.maxSteps === "number" && Number.isInteger(v1.maxSteps) && v1.maxSteps > 0 ? { steps: v1.maxSteps } : {}),
105
+ ...(model === undefined ? {} : { model }),
106
+ permissions: rules
107
+ });
108
+ const request = requestOptions(v1);
109
+ if (Object.keys(request).length > 0)
110
+ options.set(definition.id, request);
111
+ }
112
+ return { agents, requestOptions: options, hostAgents: ["build", "plan"] };
113
+ }
114
+ /** Applies a plan inside ctx.agent.transform: creates or updates each agent, hides the host agents, sets the default. */
115
+ export function applyLitAgents(editor, plan) {
116
+ for (const id of plan.hostAgents) {
117
+ editor.update(id, (agent) => {
118
+ agent.mode = "subagent";
119
+ agent.hidden = true;
120
+ agent.permissions.push({ action: "subagent", resource: "*", effect: "deny" });
121
+ });
122
+ }
123
+ for (const [id, edit] of plan.agents) {
124
+ editor.update(id, (agent) => {
125
+ const { permissions, ...fields } = edit;
126
+ Object.assign(agent, fields);
127
+ // Agent rules follow the host defaults the agent already carries; the last match wins.
128
+ agent.permissions.push(...permissions.map((rule) => ({ ...rule })));
129
+ });
130
+ }
131
+ editor.default("lit-loop");
132
+ }
133
+ /**
134
+ * The OpenAI rows LitOpenCode routes to, added to the host catalog when it lacks one, as the OpenCode 1
135
+ * `config` hook does. A row the host already has is left as the host made it, and the editor cannot
136
+ * create a provider the user has not set up. Each effort becomes a variant shaped like the host's own
137
+ * OpenAI rows, so a route such as `#xhigh` resolves.
138
+ */
139
+ export function registerOpenAiProviderModels(editor) {
140
+ for (const [modelID, metadata] of Object.entries(openaiProviderModels)) {
141
+ if (editor.get("openai", modelID) !== undefined)
142
+ continue;
143
+ editor.update("openai", modelID, (model) => {
144
+ model.name = metadata.name;
145
+ model.capabilities = { tools: metadata.tool_call, input: ["text", "image"], output: ["text"], temperature: metadata.temperature };
146
+ model.variants = openAiModelEfforts(modelID).map((effort) => ({
147
+ id: effort,
148
+ settings: { reasoningEffort: effort, reasoningSummary: "auto", include: ["reasoning.encrypted_content"] }
149
+ }));
150
+ });
151
+ }
152
+ }
153
+ /**
154
+ * OpenCode 2 runs a session that has no model of its own on the host default model: the agent's model
155
+ * is only read by a client that creates the session (the TUI does, `opencode2 run` does not). This sets
156
+ * such a root session to its agent's route, lit-loop when it names none, once the route is in the
157
+ * catalog. A session with a model, a delegated child (the host gives it its agent's model) and an agent
158
+ * LitOpenCode does not own keep the host's choice.
159
+ */
160
+ export function createSessionModelRoute(plan, session, models) {
161
+ return async (sessionID) => {
162
+ const info = await session.get({ sessionID });
163
+ if (info.model !== undefined && info.model !== null)
164
+ return;
165
+ if (typeof info.parentID === "string" && info.parentID !== "")
166
+ return;
167
+ const route = plan.agents.get(typeof info.agent === "string" && info.agent !== "" ? info.agent : "lit-loop")?.model;
168
+ if (route === undefined)
169
+ return;
170
+ const listed = await models.list();
171
+ const available = Array.isArray(listed) ? listed : listed.data;
172
+ if (!available.some((model) => model.providerID === route.providerID && model.id === route.id && model.enabled !== false))
173
+ return;
174
+ await session.switchModel({ sessionID, model: { ...route } });
175
+ };
176
+ }
177
+ /** The second lock for planning-only agents: whatever the rules say, edits, the shell, subagents and Code Mode are denied. */
178
+ export function enforcePlanningOnly(evaluation) {
179
+ if (evaluation.agent === undefined || !planningOnlyAgents.has(evaluation.agent))
180
+ return;
181
+ if (!planningDeniedAliases.has(evaluation.action))
182
+ return;
183
+ evaluation.effect = "deny";
184
+ evaluation.message = `${evaluation.agent} only plans. Ask lit-loop or run /start-work to change files or run commands.`;
185
+ }
@@ -0,0 +1,44 @@
1
+ export type CommandInvocationLike = {
2
+ readonly sessionID: string;
3
+ readonly prompt: {
4
+ readonly text: string;
5
+ };
6
+ readonly delivery: "steer" | "queue";
7
+ };
8
+ export type CommandDefinitionLike = {
9
+ readonly name: string;
10
+ readonly description?: string;
11
+ readonly execute: (input: CommandInvocationLike) => Promise<void>;
12
+ };
13
+ export type CommandEditorLike = {
14
+ add(definition: CommandDefinitionLike): void;
15
+ };
16
+ export type CommandSessionLike = {
17
+ readonly switchAgent: (input: {
18
+ readonly sessionID: string;
19
+ readonly agent: string;
20
+ }) => Promise<unknown>;
21
+ readonly prompt: (input: {
22
+ readonly sessionID: string;
23
+ readonly text: string;
24
+ readonly delivery?: "steer" | "queue";
25
+ readonly metadata?: {
26
+ readonly litopencode: {
27
+ readonly command: string;
28
+ readonly banner: string;
29
+ readonly nonce: string;
30
+ };
31
+ };
32
+ }) => Promise<unknown>;
33
+ };
34
+ /** Lit command ids that no alias file covers in the given command roots. */
35
+ export declare function pluginOwnedCommandIds(roots: readonly string[]): readonly string[];
36
+ export type CommandMark = {
37
+ readonly command: string;
38
+ readonly banner: string;
39
+ };
40
+ export declare function createV2Commands(roots: readonly string[], session: CommandSessionLike): {
41
+ definitions: CommandDefinitionLike[];
42
+ acceptMark: (sessionID: string, marker: unknown) => CommandMark | undefined;
43
+ add(editor: CommandEditorLike): void;
44
+ };
@@ -0,0 +1,65 @@
1
+ // Lit slash commands on OpenCode 2. The installer writes one markdown alias file per command
2
+ // (`command/<id>.md`), and OpenCode 2 lets such a file replace a plugin command of the same name, so the
3
+ // alias files are the source of every lit command on both hosts. The plugin adds a command only when no
4
+ // alias file covers it, for example when the package was added with `opencode plugin` and the installer
5
+ // never ran. A command the plugin sends carries a one-time nonce in the user message metadata; the
6
+ // prompt hook accepts the command mark only with that nonce, so a typed or forged mark is dropped. A
7
+ // command with an agent route (the `agent:` line of its alias file) first switches the session to that
8
+ // agent, so `/lit-crucible` really runs in the planning-only `lit-plan`.
9
+ import { randomUUID } from "node:crypto";
10
+ import fs from "node:fs";
11
+ import path from "node:path";
12
+ import { litOpenCodeCommands } from "../commands.js";
13
+ const aliasDirectories = ["command", "commands"];
14
+ function hasAliasFile(roots, id) {
15
+ return roots.some((root) => aliasDirectories.some((directory) => fs.existsSync(path.join(root, directory, `${id}.md`))));
16
+ }
17
+ /** Lit command ids that no alias file covers in the given command roots. */
18
+ export function pluginOwnedCommandIds(roots) {
19
+ return litOpenCodeCommands.map((command) => command.id).filter((id) => !hasAliasFile(roots, id));
20
+ }
21
+ export function createV2Commands(roots, session) {
22
+ const owned = new Set(pluginOwnedCommandIds(roots));
23
+ const pending = new Map();
24
+ const definitions = litOpenCodeCommands
25
+ .filter((command) => owned.has(command.id))
26
+ .map((command) => ({
27
+ name: command.id,
28
+ description: `${command.title} - ${command.description}`,
29
+ async execute(input) {
30
+ const nonce = randomUUID();
31
+ const mark = { command: command.id, banner: command.banner };
32
+ pending.set(input.sessionID, { nonce, mark });
33
+ const argumentsText = input.prompt.text.trim();
34
+ if (command.agent !== undefined)
35
+ await session.switchAgent({ sessionID: input.sessionID, agent: command.agent });
36
+ await session.prompt({
37
+ sessionID: input.sessionID,
38
+ text: argumentsText === "" ? command.activationText : `${command.activationText}\n\n${argumentsText}`,
39
+ delivery: input.delivery,
40
+ metadata: { litopencode: { ...mark, nonce } }
41
+ });
42
+ }
43
+ }));
44
+ /**
45
+ * Checks a command mark at admission. Returns the mark when the plugin sent it for this session (and
46
+ * consumes the nonce), and undefined otherwise; the caller then removes the mark from the metadata.
47
+ */
48
+ function acceptMark(sessionID, marker) {
49
+ if (typeof marker !== "object" || marker === null)
50
+ return undefined;
51
+ const expected = pending.get(sessionID);
52
+ if (expected === undefined || marker.nonce !== expected.nonce)
53
+ return undefined;
54
+ pending.delete(sessionID);
55
+ return expected.mark;
56
+ }
57
+ return {
58
+ definitions,
59
+ acceptMark,
60
+ add(editor) {
61
+ for (const definition of definitions)
62
+ editor.add(definition);
63
+ }
64
+ };
65
+ }
@@ -0,0 +1,67 @@
1
+ import type { SessionLookup } from "../session-lineage.ts";
2
+ import type { AgentEditorLike, ModelEditorLike, PermissionEvaluationLike, SessionModelDomainLike } from "./agents.ts";
3
+ import type { CommandEditorLike, CommandSessionLike } from "./commands.ts";
4
+ import type { EventDomainLike, EventModelLike, EventSessionLike } from "./events.ts";
5
+ import type { ToolAfterEvent, ToolBeforeEvent } from "./hooks.ts";
6
+ import type { ContextEvent, PromptEvent } from "./session-hooks.ts";
7
+ import type { ToolEditorLike } from "./tools.ts";
8
+ export type Registration = {
9
+ readonly dispose: () => Promise<void>;
10
+ };
11
+ export type Transform<Editor> = (callback: (editor: Editor) => void) => Promise<Registration>;
12
+ type Callback<Input> = (input: Input) => Promise<void> | void;
13
+ export type LocationLike = {
14
+ readonly directory: string;
15
+ readonly project: {
16
+ readonly directory: string;
17
+ };
18
+ };
19
+ type ToolHook = {
20
+ (name: "execute.before", callback: Callback<ToolBeforeEvent>): Promise<Registration>;
21
+ (name: "execute.after", callback: Callback<ToolAfterEvent>): Promise<Registration>;
22
+ };
23
+ export type SessionInfoLike = {
24
+ readonly parentID?: string;
25
+ readonly agent?: string;
26
+ };
27
+ type SessionHook = {
28
+ (name: "prompt", callback: Callback<PromptEvent>): Promise<Registration>;
29
+ (name: "context", callback: Callback<ContextEvent>): Promise<Registration>;
30
+ (name: "compaction", callback: Callback<ContextEvent>): Promise<Registration>;
31
+ };
32
+ export type SessionDomainLike = {
33
+ readonly get: (input: {
34
+ readonly sessionID: string;
35
+ }) => Promise<SessionInfoLike>;
36
+ };
37
+ export type ServerContextLike = {
38
+ readonly location: LocationLike;
39
+ readonly agent: {
40
+ readonly transform: Transform<AgentEditorLike>;
41
+ };
42
+ readonly tool: {
43
+ readonly transform: Transform<ToolEditorLike>;
44
+ readonly hook: ToolHook;
45
+ };
46
+ readonly permission: {
47
+ readonly hook: (name: "evaluate", callback: Callback<PermissionEvaluationLike>) => Promise<Registration>;
48
+ };
49
+ readonly command: {
50
+ readonly transform: Transform<CommandEditorLike>;
51
+ };
52
+ readonly session: SessionDomainLike & SessionModelDomainLike & CommandSessionLike & EventSessionLike & {
53
+ readonly hook: SessionHook;
54
+ };
55
+ readonly event: EventDomainLike;
56
+ readonly model: EventModelLike & {
57
+ readonly transform: Transform<ModelEditorLike>;
58
+ };
59
+ };
60
+ /** The project LitOpenCode keeps its ledger in: the project root, else the directory OpenCode opened. */
61
+ export declare function projectRootFromLocation(location: LocationLike): string;
62
+ /** Session lineage and agent from OpenCode 2; a failed lookup throws, and callers treat that as a child session. */
63
+ export declare function createSessionLookupV2(session: SessionDomainLike): SessionLookup & ((sessionID: string) => Promise<{
64
+ readonly parentID?: string;
65
+ readonly agent?: string;
66
+ }>);
67
+ export {};
@@ -0,0 +1,17 @@
1
+ /** The project LitOpenCode keeps its ledger in: the project root, else the directory OpenCode opened. */
2
+ export function projectRootFromLocation(location) {
3
+ const project = location.project.directory.trim();
4
+ if (project !== "" && project !== "/")
5
+ return project;
6
+ const directory = location.directory.trim();
7
+ return directory === "" ? "." : directory;
8
+ }
9
+ /** Session lineage and agent from OpenCode 2; a failed lookup throws, and callers treat that as a child session. */
10
+ export function createSessionLookupV2(session) {
11
+ return async (sessionID) => {
12
+ const info = await session.get({ sessionID });
13
+ const parentID = typeof info.parentID === "string" && info.parentID.length > 0 ? info.parentID : undefined;
14
+ const agent = typeof info.agent === "string" && info.agent.length > 0 ? info.agent : undefined;
15
+ return { ...(parentID === undefined ? {} : { parentID }), ...(agent === undefined ? {} : { agent }) };
16
+ };
17
+ }
@@ -0,0 +1,55 @@
1
+ import { type BoundedAuthorityOptions } from "../bounded-authority.ts";
2
+ import type { AutoHandoffConfig } from "../config.ts";
3
+ import type { SessionLookup } from "../session-lineage.ts";
4
+ import type { RuntimePaths } from "../state.ts";
5
+ export type StreamEvent = {
6
+ readonly type: string;
7
+ readonly data?: unknown;
8
+ };
9
+ export type EventDomainLike = {
10
+ readonly subscribe: (options?: {
11
+ readonly signal?: AbortSignal;
12
+ }) => AsyncIterable<StreamEvent>;
13
+ };
14
+ export type ModelInfoLike = {
15
+ readonly providerID: string;
16
+ readonly modelID?: string;
17
+ readonly id: string;
18
+ readonly limit?: {
19
+ readonly context?: number;
20
+ };
21
+ };
22
+ export type EventSessionLike = {
23
+ readonly prompt: (input: {
24
+ readonly sessionID: string;
25
+ readonly text: string;
26
+ readonly id?: string;
27
+ }) => Promise<unknown>;
28
+ readonly compact: (input: {
29
+ readonly sessionID: string;
30
+ }) => Promise<unknown>;
31
+ };
32
+ export type EventModelLike = {
33
+ readonly list: () => Promise<readonly ModelInfoLike[] | {
34
+ readonly data: readonly ModelInfoLike[];
35
+ }>;
36
+ };
37
+ export type V2EventOptions = {
38
+ readonly projectRoot: string;
39
+ readonly paths: Pick<RuntimePaths, "autoHandoffFile">;
40
+ readonly autoHandoffConfig: AutoHandoffConfig;
41
+ readonly boundedAuthority: BoundedAuthorityOptions;
42
+ readonly getSession: SessionLookup;
43
+ readonly session: EventSessionLike;
44
+ readonly model: EventModelLike;
45
+ readonly onSessionDeleted: (sessionID: string) => void;
46
+ };
47
+ export declare function createV2Events(options: V2EventOptions): {
48
+ handle: (event: StreamEvent) => Promise<void>;
49
+ start: (events: EventDomainLike) => () => Promise<void>;
50
+ /** Steps started and not yet ended. */
51
+ pendingSteps: () => number;
52
+ route: (text: string, mode: import("../auto-handoff.ts").AutoHandoffRouteMode) => Promise<string | undefined>;
53
+ /** The saved handoff, handed back once after a compaction. */
54
+ systemText(sessionID: string): Promise<readonly string[]>;
55
+ };