@litfamily/litopencode 1.0.29 → 1.0.31
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/CHANGELOG.md +22 -0
- package/README-Ko-KR.md +7 -7
- package/README.md +7 -7
- package/dist/activation-primary-prompts.js +15 -6
- package/dist/activation-prompt-utils.d.ts +2 -2
- package/dist/activation-prompt-utils.js +9 -3
- package/dist/activation-reentry.d.ts +29 -0
- package/dist/activation-reentry.js +71 -0
- package/dist/activation-routing.d.ts +1 -1
- package/dist/activation-routing.js +40 -7
- package/dist/activation-workflow-prompts.d.ts +2 -0
- package/dist/activation-workflow-prompts.js +32 -7
- package/dist/activation.d.ts +6 -1
- package/dist/activation.js +34 -7
- package/dist/cli/args.js +14 -0
- package/dist/cli/auto-update.js +2 -4
- package/dist/cli/command-plan.d.ts +40 -0
- package/dist/cli/command-plan.js +62 -0
- package/dist/cli/doctor.d.ts +4 -1
- package/dist/cli/doctor.js +65 -4
- package/dist/cli/host-detect.d.ts +58 -0
- package/dist/cli/host-detect.js +199 -0
- package/dist/cli/install-report.js +3 -1
- package/dist/cli/install.d.ts +9 -1
- package/dist/cli/install.js +134 -31
- package/dist/cli/managed-skill-assets.d.ts +1 -1
- package/dist/cli/managed-skill-assets.js +5 -3
- package/dist/cli/office-dependencies.d.ts +2 -0
- package/dist/cli/office-dependencies.js +7 -3
- package/dist/cli/scientific-visualization-dependencies.js +1 -1
- package/dist/cli/tui-config.d.ts +27 -2
- package/dist/cli/tui-config.js +77 -2
- package/dist/cli/types.d.ts +17 -0
- package/dist/cli.js +9 -6
- package/dist/commands.d.ts +10 -1
- package/dist/commands.js +17 -3
- package/dist/features.d.ts +27 -1
- package/dist/features.js +40 -8
- package/dist/host/server.d.ts +7 -0
- package/dist/host/server.js +12 -0
- package/dist/host/tui.d.ts +7 -0
- package/dist/host/tui.js +12 -0
- package/dist/server.js +9 -0
- package/dist/skill-renames.d.ts +1 -1
- package/dist/skills.d.ts +9 -2
- package/dist/skills.js +22 -8
- package/dist/tui/mission-source-v2.d.ts +51 -0
- package/dist/tui/mission-source-v2.js +52 -0
- package/dist/v2/agents.d.ts +105 -0
- package/dist/v2/agents.js +185 -0
- package/dist/v2/commands.d.ts +44 -0
- package/dist/v2/commands.js +65 -0
- package/dist/v2/context.d.ts +67 -0
- package/dist/v2/context.js +17 -0
- package/dist/v2/events.d.ts +55 -0
- package/dist/v2/events.js +193 -0
- package/dist/v2/hooks.d.ts +39 -0
- package/dist/v2/hooks.js +61 -0
- package/dist/v2/server.d.ts +2 -0
- package/dist/v2/server.js +68 -0
- package/dist/v2/session-hooks.d.ts +64 -0
- package/dist/v2/session-hooks.js +230 -0
- package/dist/v2/tools.d.ts +26 -0
- package/dist/v2/tools.js +49 -0
- package/dist/v2/tui.d.ts +63 -0
- package/dist/v2/tui.js +259 -0
- package/docs/assets/cover-motion-still.webp +0 -0
- package/docs/assets/cover-motion.webp +0 -0
- package/docs/assets/cover.webp +0 -0
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/assets/readme/litfamily-machines.png +0 -0
- package/docs/migration.md +4 -3
- package/docs/reference-Ko-KR.md +109 -7
- package/docs/reference.md +122 -9
- package/package.json +10 -5
- package/skills/deep-interview/SKILL.md +1 -1
- package/skills/lit-brainstorm/SKILL.md +299 -0
- package/skills/lit-brainstorm/references/handoff-briefs.md +72 -0
- package/skills/lit-brainstorm/references/lenses.md +106 -0
- package/skills/lit-brainstorm/references/scoring-and-convergence.md +93 -0
- package/skills/lit-brainstorm/references/session-notes-and-lanes.md +122 -0
- package/skills/lit-docx/SKILL.md +1 -1
- package/skills/lit-docx/run.py +42 -4
- package/skills/lit-docx/scripts/convert_md_to_pdf.py +38 -0
- package/skills/lit-docx/templates/latex/acs/template.tex +2 -0
- package/skills/lit-docx/templates/latex/elsevier/template.tex +2 -0
- package/skills/lit-docx/templates/latex/ieee/template.tex +2 -0
- package/skills/lit-docx/templates/latex/korean-generic/template.tex +2 -0
- package/skills/lit-docx/templates/latex/nature/template.tex +2 -0
- package/skills/lit-humanizer/scripts/detect.mjs +1 -1
- package/skills/lit-pptx/SKILL.md +1 -1
- package/skills/lit-pptx/command.mjs +94 -0
- package/skills/lit-pptx/run.mjs +16 -6
- package/skills/lit-pptx/scripts/compile-deck.js +2 -2
- package/skills/lit-typographic-motion/SKILL.md +5 -2
- package/skills/lit-typographic-motion/THIRD_PARTY_NOTICES +3 -0
- package/skills/lit-typographic-motion/engine/browser.mjs +3 -9
- package/skills/lit-typographic-motion/engine/command.mjs +94 -0
- package/skills/lit-typographic-motion/engine/constants.mjs +29 -1
- package/skills/lit-typographic-motion/engine/cover/assets.mjs +91 -0
- package/skills/lit-typographic-motion/engine/cover/capture.mjs +173 -0
- package/skills/lit-typographic-motion/engine/cover/decode.mjs +173 -0
- package/skills/lit-typographic-motion/engine/cover/frame-worker.mjs +19 -0
- package/skills/lit-typographic-motion/engine/cover/gate.mjs +261 -0
- package/skills/lit-typographic-motion/engine/cover/image.mjs +138 -0
- package/skills/lit-typographic-motion/engine/cover/render.mjs +323 -0
- package/skills/lit-typographic-motion/engine/cover/sheet.mjs +65 -0
- package/skills/lit-typographic-motion/engine/cover/treatment.mjs +110 -0
- package/skills/lit-typographic-motion/engine/encode.mjs +1 -1
- package/skills/lit-typographic-motion/engine/look.mjs +113 -10
- package/skills/lit-typographic-motion/engine/sound.mjs +1 -1
- package/skills/lit-typographic-motion/engine/stage/capture.mjs +26 -18
- package/skills/lit-typographic-motion/engine/stage/cover-init.js +38 -0
- package/skills/lit-typographic-motion/engine/stage/loop-kit.js +102 -0
- package/skills/lit-typographic-motion/engine/stage/render.mjs +4 -4
- package/skills/lit-typographic-motion/engine/treatment.mjs +1 -0
- package/skills/lit-typographic-motion/fonts/JetBrainsMono-OFL.txt +93 -0
- package/skills/lit-typographic-motion/fonts/JetBrainsMono-wght-latin.woff2 +0 -0
- package/skills/lit-typographic-motion/fonts/pins.json +3 -1
- package/skills/lit-typographic-motion/gate.mjs +34 -1
- package/skills/lit-typographic-motion/probe.mjs +8 -3
- package/skills/lit-typographic-motion/recipes/negatives/card-scenes/expected.json +16 -0
- package/skills/lit-typographic-motion/recipes/negatives/card-scenes/index.html +35 -0
- package/skills/lit-typographic-motion/recipes/negatives/reset-sweep/expected.json +16 -0
- package/skills/lit-typographic-motion/recipes/negatives/reset-sweep/index.html +29 -0
- package/skills/lit-typographic-motion/recipes/negatives/still-glow/expected.json +17 -0
- package/skills/lit-typographic-motion/recipes/negatives/still-glow/index.html +29 -0
- package/skills/lit-typographic-motion/recipes/negatives/title-slam/expected.json +17 -0
- package/skills/lit-typographic-motion/recipes/negatives/title-slam/index.html +35 -0
- package/skills/lit-typographic-motion/recipes/orbit-loop/expected.json +62 -0
- package/skills/lit-typographic-motion/recipes/orbit-loop/index.html +100 -0
- package/skills/lit-typographic-motion/recipes/painted-light/expected.json +95 -0
- package/skills/lit-typographic-motion/recipes/painted-light/index.html +232 -0
- package/skills/lit-typographic-motion/recipes/weight-tide/expected.json +62 -0
- package/skills/lit-typographic-motion/recipes/weight-tide/index.html +81 -0
- package/skills/lit-typographic-motion/references/cover.md +423 -0
- package/skills/lit-typographic-motion/references/craft-loop.md +2 -0
- package/skills/lit-typographic-motion/references/runtime.md +17 -2
- package/skills/lit-typographic-motion/references/stage.md +3 -0
- package/skills/lit-typographic-motion/references/treatment.md +4 -0
- package/skills/lit-typographic-motion/render.mjs +30 -6
- package/skills/lit-typographic-motion/requirements-audio.txt +3 -3
- package/skills/lit-typographic-motion/runtime.mjs +12 -13
- package/skills/{durable-litgoal → litgoal}/SKILL.md +31 -12
- package/skills/managed-skill-manifest.json +176 -32
- package/skills/readme-studio/templates/hyperframes/package-lock.json +3 -3
- package/skills/readme-studio/templates/remotion/package-lock.json +3 -3
- package/skills/review-work/SKILL.md +27 -18
- package/skills/skill-rename-aliases.json +2 -1
- package/tools/check-pack-payload.mjs +4 -1
- package/tools/payload-substance-allowlist.json +1 -1
- package/tools/payload-substance-parity.json +6 -4
- package/tools/qa-cover-recipes.mjs +167 -0
- package/tools/version-manifests.json +6 -0
package/dist/v2/hooks.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Tool hooks on OpenCode 2, reusing the OpenCode 1 guards. OpenCode 2 renamed two tools (bash is shell,
|
|
2
|
+
// task is subagent) and names a file argument `path`, so each call is shown to the guards under the
|
|
3
|
+
// names they know. A throw in `execute.before` fails that one call; `execute.after` must never throw.
|
|
4
|
+
import { createToolExecuteAfterHook, createToolExecuteBeforeHook } from "../hooks.js";
|
|
5
|
+
const v1ToolNames = { shell: "bash", subagent: "task" };
|
|
6
|
+
function v1ToolName(tool) {
|
|
7
|
+
return v1ToolNames[tool] ?? tool;
|
|
8
|
+
}
|
|
9
|
+
function isRecord(value) {
|
|
10
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
/** The arguments as the OpenCode 1 guards read them: a `path` also answers to `filePath`. */
|
|
13
|
+
function v1Args(input) {
|
|
14
|
+
if (!isRecord(input) || typeof input.path !== "string" || "filePath" in input)
|
|
15
|
+
return { args: input, aliased: false };
|
|
16
|
+
return { args: { ...input, filePath: input.path }, aliased: true };
|
|
17
|
+
}
|
|
18
|
+
function contentText(content) {
|
|
19
|
+
if (content === undefined)
|
|
20
|
+
return "";
|
|
21
|
+
if (typeof content === "string")
|
|
22
|
+
return content;
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
export function createV2ToolHooks(options) {
|
|
26
|
+
const before = createToolExecuteBeforeHook({ getSession: options.getSession, deliverableHedgeGuard: options.deliverableHedgeGuard });
|
|
27
|
+
const after = createToolExecuteAfterHook({
|
|
28
|
+
projectRoot: options.projectRoot,
|
|
29
|
+
knowledgeCaptureEnabled: options.knowledgeCaptureEnabled,
|
|
30
|
+
deliverableHedgeGuard: options.deliverableHedgeGuard
|
|
31
|
+
});
|
|
32
|
+
async function executeBefore(event) {
|
|
33
|
+
const { args, aliased } = v1Args(event.input);
|
|
34
|
+
const output = { args };
|
|
35
|
+
await before({ tool: v1ToolName(event.tool), sessionID: event.sessionID, callID: event.id }, output);
|
|
36
|
+
if (output.args === args)
|
|
37
|
+
return;
|
|
38
|
+
if (aliased && isRecord(output.args)) {
|
|
39
|
+
const { filePath: _alias, ...rest } = output.args;
|
|
40
|
+
event.input = rest;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
event.input = output.args;
|
|
44
|
+
}
|
|
45
|
+
async function executeAfter(event) {
|
|
46
|
+
if (event.status !== "completed" || event.result === undefined)
|
|
47
|
+
return;
|
|
48
|
+
const text = contentText(event.result.content);
|
|
49
|
+
if (text === undefined)
|
|
50
|
+
return;
|
|
51
|
+
const output = { title: "", output: text, metadata: { ...(event.result.metadata ?? {}) } };
|
|
52
|
+
try {
|
|
53
|
+
await after({ tool: v1ToolName(event.tool), sessionID: event.sessionID, callID: event.id, args: v1Args(event.input).args }, output);
|
|
54
|
+
}
|
|
55
|
+
catch { // no-excuse-ok: catch -- OpenCode 2 treats a throw after a tool ran as a crash; the result stays as it was.
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
event.result = { ...event.result, content: output.output, metadata: output.metadata };
|
|
59
|
+
}
|
|
60
|
+
return { executeBefore, executeAfter };
|
|
61
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// LitOpenCode's OpenCode 2 server plugin. OpenCode 2 runs one instance per project location and calls
|
|
2
|
+
// `setup` once for it; every registration below is released by the host when the instance stops, and
|
|
3
|
+
// the returned cleanup releases what the plugin itself holds. There is no automatic self-update here:
|
|
4
|
+
// OpenCode 2 updates plugins itself (`opencode plugin update` and its update policy).
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { loadConfig } from "../config.js";
|
|
7
|
+
import { createDeliverableHedgeGuard } from "../deliverable-hedge-guard.js";
|
|
8
|
+
import { createLogger } from "../logger.js";
|
|
9
|
+
import { defaultOpenCodeConfigRoot } from "../state.js";
|
|
10
|
+
import { applyLitAgents, createSessionModelRoute, enforcePlanningOnly, planLitAgents, registerOpenAiProviderModels } from "./agents.js";
|
|
11
|
+
import { createSessionLookupV2, projectRootFromLocation } from "./context.js";
|
|
12
|
+
import { createV2Commands } from "./commands.js";
|
|
13
|
+
import { createV2Events } from "./events.js";
|
|
14
|
+
import { createV2ToolHooks } from "./hooks.js";
|
|
15
|
+
import { createV2SessionHooks } from "./session-hooks.js";
|
|
16
|
+
import { addLitTools, litToolsForProject } from "./tools.js";
|
|
17
|
+
export async function setupLitOpenCodeServer(context) {
|
|
18
|
+
const root = projectRootFromLocation(context.location);
|
|
19
|
+
const loaded = await loadConfig(root);
|
|
20
|
+
const logger = createLogger(loaded.paths);
|
|
21
|
+
const getSession = createSessionLookupV2(context.session);
|
|
22
|
+
const deliverableHedgeGuard = createDeliverableHedgeGuard({ projectRoot: context.location.directory.trim() || root });
|
|
23
|
+
const agentPlan = planLitAgents(loaded.config);
|
|
24
|
+
const toolHooks = createV2ToolHooks({
|
|
25
|
+
projectRoot: root,
|
|
26
|
+
knowledgeCaptureEnabled: loaded.config.knowledge.capture,
|
|
27
|
+
getSession,
|
|
28
|
+
deliverableHedgeGuard
|
|
29
|
+
});
|
|
30
|
+
await context.model.transform(registerOpenAiProviderModels);
|
|
31
|
+
await context.agent.transform((editor) => applyLitAgents(editor, agentPlan));
|
|
32
|
+
await context.permission.hook("evaluate", enforcePlanningOnly);
|
|
33
|
+
const tools = litToolsForProject(root);
|
|
34
|
+
await context.tool.transform((editor) => addLitTools(editor, tools));
|
|
35
|
+
await context.tool.hook("execute.before", toolHooks.executeBefore);
|
|
36
|
+
await context.tool.hook("execute.after", toolHooks.executeAfter);
|
|
37
|
+
const commands = createV2Commands([defaultOpenCodeConfigRoot(), path.join(root, ".opencode")], context.session);
|
|
38
|
+
await context.command.transform((editor) => commands.add(editor));
|
|
39
|
+
const events = createV2Events({
|
|
40
|
+
projectRoot: root,
|
|
41
|
+
paths: loaded.paths,
|
|
42
|
+
autoHandoffConfig: loaded.config.autoHandoff,
|
|
43
|
+
boundedAuthority: loaded.config.boundedAuthority,
|
|
44
|
+
getSession,
|
|
45
|
+
session: context.session,
|
|
46
|
+
model: context.model,
|
|
47
|
+
onSessionDeleted: (sessionID) => sessionHooks.clear(sessionID)
|
|
48
|
+
});
|
|
49
|
+
const sessionHooks = createV2SessionHooks({
|
|
50
|
+
projectRoot: root,
|
|
51
|
+
config: loaded.config,
|
|
52
|
+
getSession,
|
|
53
|
+
routeModel: createSessionModelRoute(agentPlan, context.session, context.model),
|
|
54
|
+
requestOptions: agentPlan.requestOptions,
|
|
55
|
+
autoHandoffRoute: events.route,
|
|
56
|
+
extraSystem: events.systemText,
|
|
57
|
+
acceptCommandMark: commands.acceptMark
|
|
58
|
+
});
|
|
59
|
+
await context.session.hook("prompt", sessionHooks.prompt);
|
|
60
|
+
await context.session.hook("context", sessionHooks.context);
|
|
61
|
+
await context.session.hook("compaction", sessionHooks.compaction);
|
|
62
|
+
const stopEvents = events.start(context.event);
|
|
63
|
+
return async () => {
|
|
64
|
+
await stopEvents();
|
|
65
|
+
deliverableHedgeGuard.dispose();
|
|
66
|
+
await logger.dispose();
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type ChatActivationMode } from "../activation-routing.ts";
|
|
2
|
+
import { type AutoHandoffRouteMode } from "../auto-handoff.ts";
|
|
3
|
+
import type { LitOpenCodeConfig } from "../config.ts";
|
|
4
|
+
import type { AgentRequestOptions } from "./agents.ts";
|
|
5
|
+
export type SystemPartLike = {
|
|
6
|
+
readonly type: "text";
|
|
7
|
+
readonly text: string;
|
|
8
|
+
};
|
|
9
|
+
export type PromptEvent = {
|
|
10
|
+
readonly sessionID: string;
|
|
11
|
+
readonly messageID: string;
|
|
12
|
+
prompt: {
|
|
13
|
+
text: string;
|
|
14
|
+
};
|
|
15
|
+
metadata?: Record<string, unknown>;
|
|
16
|
+
};
|
|
17
|
+
/** An OpenCode 2 request message: an instance of the host's own message class. */
|
|
18
|
+
export type MessageLike = {
|
|
19
|
+
readonly role: string;
|
|
20
|
+
readonly content: readonly unknown[];
|
|
21
|
+
};
|
|
22
|
+
export type ContextEvent = {
|
|
23
|
+
readonly sessionID: string;
|
|
24
|
+
readonly agent: string;
|
|
25
|
+
system: SystemPartLike[];
|
|
26
|
+
messages?: MessageLike[];
|
|
27
|
+
options: Record<string, unknown>;
|
|
28
|
+
};
|
|
29
|
+
export type SessionAgentLookup = (sessionID: string) => Promise<{
|
|
30
|
+
readonly parentID?: string;
|
|
31
|
+
readonly agent?: string;
|
|
32
|
+
} | undefined>;
|
|
33
|
+
export type V2SessionHookOptions = {
|
|
34
|
+
readonly projectRoot: string;
|
|
35
|
+
readonly config: Pick<LitOpenCodeConfig, "outputStyle" | "boundedAuthority">;
|
|
36
|
+
readonly getSession: SessionAgentLookup;
|
|
37
|
+
/** Puts a session that has no model of its own on its agent's route. */
|
|
38
|
+
readonly routeModel?: (sessionID: string) => Promise<void>;
|
|
39
|
+
readonly requestOptions: ReadonlyMap<string, AgentRequestOptions>;
|
|
40
|
+
readonly autoHandoffRoute?: (text: string, mode: AutoHandoffRouteMode) => Promise<string | undefined>;
|
|
41
|
+
/** Extra system text for this request, such as the saved handoff after a compaction. */
|
|
42
|
+
readonly extraSystem?: (sessionID: string) => Promise<readonly string[]>;
|
|
43
|
+
readonly onActivation?: (sessionID: string, mode: ChatActivationMode | string) => void;
|
|
44
|
+
/** Accepts a command mark the plugin itself sent for this session; anything else is dropped. */
|
|
45
|
+
readonly acceptCommandMark?: (sessionID: string, marker: unknown) => {
|
|
46
|
+
readonly command: string;
|
|
47
|
+
readonly banner: string;
|
|
48
|
+
} | undefined;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* `opencode2 run "lit fix it"` sends the message wrapped in double quotes (each multi-word argument is
|
|
52
|
+
* quoted, inner quotes escaped). The prompt hook runs in the server, so the run's argv is not available
|
|
53
|
+
* the way it is on OpenCode 1; a message that is exactly one such quoted argument is read without the
|
|
54
|
+
* quotes. Quoted text inside a longer prompt stays inactive. The server cannot tell a run argument from
|
|
55
|
+
* a TUI prompt typed entirely inside double quotes, so such a typed `"lit fix it"` activates as well;
|
|
56
|
+
* on OpenCode 1 it stayed inert.
|
|
57
|
+
*/
|
|
58
|
+
export declare function unwrapRunArgument(input: string): string;
|
|
59
|
+
export declare function createV2SessionHooks(options: V2SessionHookOptions): {
|
|
60
|
+
prompt: (event: PromptEvent) => Promise<void>;
|
|
61
|
+
context: (event: ContextEvent) => Promise<void>;
|
|
62
|
+
compaction: (event: ContextEvent) => Promise<void>;
|
|
63
|
+
clear(sessionID: string): void;
|
|
64
|
+
};
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// Prompt admission and request context on OpenCode 2. OpenCode 1 appended activation text to the user
|
|
2
|
+
// message as an extra part; OpenCode 2 has no parts. Here the `prompt` hook marks a lit prompt in the
|
|
3
|
+
// user message metadata (which the TUI reads for the ignition toast and the footer line), and the
|
|
4
|
+
// `context` hook hands the activation text, the static rules and the reader contract to every model
|
|
5
|
+
// request of that session as system parts. Text that comes from the project or a lifecycle file rather
|
|
6
|
+
// than from LitOpenCode's own instructions (local knowledge, a start-work receipt, an auto-handoff setting
|
|
7
|
+
// reply) rides the latest user message instead, the channel it had on OpenCode 1.
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { createActivationMemory, decideReentry } from "../activation-reentry.js";
|
|
10
|
+
import { detectChatActivationMode, promptForChatActivationMode } from "../activation-routing.js";
|
|
11
|
+
import { startWorkPromptInjection } from "../activation-workflow-prompts.js";
|
|
12
|
+
import { autoHandoffSettingText } from "../auto-handoff.js";
|
|
13
|
+
import { applyTrustedStartWorkDirective, parseStartWorkLifecycleDirective } from "../bounded-authority-hooks.js";
|
|
14
|
+
import { queryKnowledge } from "../knowledge.js";
|
|
15
|
+
import { createLitGoalOperations } from "../ledger.js";
|
|
16
|
+
import { applyCompactionRuleReset, applyStaticRuleInjection } from "../rules/hooks.js";
|
|
17
|
+
import { isReadOnlyWorkflowFamilyPlan } from "../workflow-families.js";
|
|
18
|
+
const bundledRulesDir = fileURLToPath(new URL("../../rules/bundled-rules", import.meta.url));
|
|
19
|
+
/**
|
|
20
|
+
* `opencode2 run "lit fix it"` sends the message wrapped in double quotes (each multi-word argument is
|
|
21
|
+
* quoted, inner quotes escaped). The prompt hook runs in the server, so the run's argv is not available
|
|
22
|
+
* the way it is on OpenCode 1; a message that is exactly one such quoted argument is read without the
|
|
23
|
+
* quotes. Quoted text inside a longer prompt stays inactive. The server cannot tell a run argument from
|
|
24
|
+
* a TUI prompt typed entirely inside double quotes, so such a typed `"lit fix it"` activates as well;
|
|
25
|
+
* on OpenCode 1 it stayed inert.
|
|
26
|
+
*/
|
|
27
|
+
export function unwrapRunArgument(input) {
|
|
28
|
+
const match = /^"((?:[^"\\]|\\.)*)"$/su.exec(input);
|
|
29
|
+
return match === null ? input : match[1].replace(/\\"/gu, '"');
|
|
30
|
+
}
|
|
31
|
+
function text(value) {
|
|
32
|
+
return { type: "text", text: value };
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Adds text parts to the latest user message. The host builds each message with its own class, so the
|
|
36
|
+
* copy is made with that class; false when there is no user message to carry the text.
|
|
37
|
+
*/
|
|
38
|
+
function appendToLatestUserMessage(messages, texts) {
|
|
39
|
+
if (messages === undefined)
|
|
40
|
+
return false;
|
|
41
|
+
let index = messages.length - 1;
|
|
42
|
+
while (index >= 0 && messages[index].role !== "user")
|
|
43
|
+
index -= 1;
|
|
44
|
+
if (index < 0)
|
|
45
|
+
return false;
|
|
46
|
+
const latest = messages[index];
|
|
47
|
+
const Message = latest.constructor;
|
|
48
|
+
messages[index] = new Message({ ...latest, content: [...latest.content, ...texts.map(text)] });
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
export function createV2SessionHooks(options) {
|
|
52
|
+
const operations = createLitGoalOperations(options.projectRoot);
|
|
53
|
+
const sessions = new Map();
|
|
54
|
+
// The latest activation stays in every request of its session, so a repeated lit only adds a note to it.
|
|
55
|
+
const activationMemory = createActivationMemory();
|
|
56
|
+
function stateFor(sessionID) {
|
|
57
|
+
let state = sessions.get(sessionID);
|
|
58
|
+
if (state === undefined) {
|
|
59
|
+
state = { turn: [], user: [] };
|
|
60
|
+
sessions.set(sessionID, state);
|
|
61
|
+
}
|
|
62
|
+
return state;
|
|
63
|
+
}
|
|
64
|
+
async function lookup(sessionID) {
|
|
65
|
+
try {
|
|
66
|
+
const session = await options.getSession(sessionID);
|
|
67
|
+
if (session === undefined)
|
|
68
|
+
return { child: true };
|
|
69
|
+
return { child: session.parentID !== undefined, ...(session.agent === undefined ? {} : { agent: session.agent }) };
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Fail closed: a session whose lineage cannot be read is treated as delegated.
|
|
73
|
+
return { child: true };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Keeps a command mark only when the plugin sent it; returns true for such a trusted command turn. */
|
|
77
|
+
function admitCommandMark(event) {
|
|
78
|
+
const metadata = event.metadata;
|
|
79
|
+
if (metadata === undefined || !("litopencode" in metadata))
|
|
80
|
+
return false;
|
|
81
|
+
const { litopencode: marker, ...rest } = metadata;
|
|
82
|
+
const accepted = options.acceptCommandMark?.(event.sessionID, marker);
|
|
83
|
+
if (accepted === undefined) {
|
|
84
|
+
event.metadata = Object.keys(rest).length === 0 ? undefined : rest;
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
event.metadata = { ...rest, litopencode: { command: accepted.command, banner: accepted.banner, source: "command" } };
|
|
88
|
+
options.onActivation?.(event.sessionID, accepted.command);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
async function prompt(event) {
|
|
92
|
+
const input = unwrapRunArgument(event.prompt.text);
|
|
93
|
+
try {
|
|
94
|
+
await options.routeModel?.(event.sessionID);
|
|
95
|
+
}
|
|
96
|
+
catch { // no-excuse-ok: catch -- a failed route switch must never block the user message.
|
|
97
|
+
// The session keeps the model the host chose.
|
|
98
|
+
}
|
|
99
|
+
if (admitCommandMark(event)) {
|
|
100
|
+
const state = stateFor(event.sessionID);
|
|
101
|
+
state.turn = [];
|
|
102
|
+
state.user = [];
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const session = await lookup(event.sessionID);
|
|
106
|
+
if (session.child)
|
|
107
|
+
return;
|
|
108
|
+
const state = stateFor(event.sessionID);
|
|
109
|
+
state.turn = [];
|
|
110
|
+
state.user = [];
|
|
111
|
+
if (input.trim() === "")
|
|
112
|
+
return;
|
|
113
|
+
if (options.autoHandoffRoute !== undefined) {
|
|
114
|
+
const reply = await options.autoHandoffRoute(input, "chat");
|
|
115
|
+
if (reply !== undefined) {
|
|
116
|
+
state.user.push(autoHandoffSettingText(reply));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const knowledge = await queryKnowledge(options.projectRoot, input.slice(0, 4096));
|
|
122
|
+
if (knowledge.text !== "")
|
|
123
|
+
state.user.push(knowledge.text);
|
|
124
|
+
}
|
|
125
|
+
catch { // no-excuse-ok: catch -- local relevance must never block the user message.
|
|
126
|
+
// A malformed or unreadable knowledge store stays silent instead of inventing context.
|
|
127
|
+
}
|
|
128
|
+
const mode = detectChatActivationMode(input, session.agent);
|
|
129
|
+
if (mode === undefined)
|
|
130
|
+
return;
|
|
131
|
+
const trigger = mode === "lit-handoff" ? "handoff" : mode === "lit-scientific-visualization" ? mode : "lit";
|
|
132
|
+
let activation = promptForChatActivationMode(mode, input);
|
|
133
|
+
if (mode === "start-work" && session.agent !== "lit-plan") {
|
|
134
|
+
const directive = parseStartWorkLifecycleDirective(input, { chat: true });
|
|
135
|
+
if (directive?.action === "resume") {
|
|
136
|
+
try {
|
|
137
|
+
const receipt = await applyTrustedStartWorkDirective(options.projectRoot, event.sessionID, directive, options.config.boundedAuthority);
|
|
138
|
+
state.user.push(receipt.text);
|
|
139
|
+
activation = startWorkPromptInjection;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
activation = "BLOCKED: bounded-authority lifecycle resume rejected. Use /start-work with the exact pending boundary, grant, work id, session, and CAS revision.";
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const reentry = await decideReentry({
|
|
147
|
+
memory: activationMemory,
|
|
148
|
+
projectRoot: options.projectRoot,
|
|
149
|
+
sessionID: event.sessionID,
|
|
150
|
+
mode,
|
|
151
|
+
text: input
|
|
152
|
+
});
|
|
153
|
+
if (reentry.kind === "continue") {
|
|
154
|
+
state.turn.push(reentry.text);
|
|
155
|
+
event.metadata = { ...(event.metadata ?? {}), litopencode: { mode: reentry.mode, trigger, source: "prompt", reentry: "continue" } };
|
|
156
|
+
options.onActivation?.(event.sessionID, reentry.mode);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (reentry.kind === "resume")
|
|
160
|
+
activation = `${reentry.line}\n${activation}`;
|
|
161
|
+
else if (reentry.kind === "confirm")
|
|
162
|
+
activation = `${activation}\n${reentry.line}`;
|
|
163
|
+
state.activation = activation;
|
|
164
|
+
event.metadata = {
|
|
165
|
+
...(event.metadata ?? {}),
|
|
166
|
+
litopencode: { mode, trigger, source: "prompt", ...(reentry.kind === "full" ? {} : { reentry: reentry.kind }) }
|
|
167
|
+
};
|
|
168
|
+
options.onActivation?.(event.sessionID, mode);
|
|
169
|
+
if (reentry.kind !== "confirm" && mode !== "lit-handoff" && mode !== "lit-scientific-visualization") {
|
|
170
|
+
activationMemory.remember(event.sessionID, mode);
|
|
171
|
+
}
|
|
172
|
+
if (mode === "lit-task" || mode === "lit-recap" || mode === "lit-brainstorm" || isReadOnlyWorkflowFamilyPlan(mode))
|
|
173
|
+
return;
|
|
174
|
+
try {
|
|
175
|
+
await operations.init();
|
|
176
|
+
await operations.append({
|
|
177
|
+
type: "prompt.activated",
|
|
178
|
+
trigger,
|
|
179
|
+
source: "prompt",
|
|
180
|
+
mode,
|
|
181
|
+
sessionID: event.sessionID,
|
|
182
|
+
messageID: event.messageID,
|
|
183
|
+
...(session.agent === undefined ? {} : { agent: session.agent }),
|
|
184
|
+
timestamp: new Date().toISOString()
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
catch { // no-excuse-ok: catch -- activation must survive best-effort ledger failure.
|
|
188
|
+
// The activation text is the primary surface; the ledger write is best-effort.
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async function context(event) {
|
|
192
|
+
const state = sessions.get(event.sessionID);
|
|
193
|
+
if (state?.activation !== undefined)
|
|
194
|
+
event.system.push(text(state.activation));
|
|
195
|
+
for (const entry of state?.turn ?? [])
|
|
196
|
+
event.system.push(text(entry));
|
|
197
|
+
const user = state?.user ?? [];
|
|
198
|
+
if (user.length > 0 && !appendToLatestUserMessage(event.messages, user)) {
|
|
199
|
+
for (const entry of user)
|
|
200
|
+
event.system.push(text(entry));
|
|
201
|
+
}
|
|
202
|
+
for (const entry of (await options.extraSystem?.(event.sessionID)) ?? [])
|
|
203
|
+
event.system.push(text(entry));
|
|
204
|
+
const rules = { system: [] };
|
|
205
|
+
await applyStaticRuleInjection({ projectRoot: options.projectRoot, bundledRulesDir, outputStyle: options.config.outputStyle }, { sessionID: event.sessionID }, rules);
|
|
206
|
+
for (const entry of rules.system)
|
|
207
|
+
event.system.push(text(entry));
|
|
208
|
+
const request = options.requestOptions.get(event.agent);
|
|
209
|
+
if (request !== undefined) {
|
|
210
|
+
for (const [key, value] of Object.entries(request))
|
|
211
|
+
if (event.options[key] === undefined)
|
|
212
|
+
event.options[key] = value;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async function compaction(event) {
|
|
216
|
+
const output = { context: [] };
|
|
217
|
+
await applyCompactionRuleReset({ sessionID: event.sessionID }, output);
|
|
218
|
+
for (const entry of output.context)
|
|
219
|
+
event.system.push(text(entry));
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
prompt,
|
|
223
|
+
context,
|
|
224
|
+
compaction,
|
|
225
|
+
clear(sessionID) {
|
|
226
|
+
sessions.delete(sessionID);
|
|
227
|
+
activationMemory.forget(sessionID);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ToolDefinition } from "@opencode-ai/plugin";
|
|
2
|
+
export type ToolCallContextLike = {
|
|
3
|
+
readonly sessionID: string;
|
|
4
|
+
readonly agent: string;
|
|
5
|
+
readonly messageID: string;
|
|
6
|
+
readonly signal: AbortSignal;
|
|
7
|
+
};
|
|
8
|
+
export type ToolResultLike = {
|
|
9
|
+
readonly content?: string;
|
|
10
|
+
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
11
|
+
};
|
|
12
|
+
export type ToolInfoLike = {
|
|
13
|
+
readonly name: string;
|
|
14
|
+
readonly description: string;
|
|
15
|
+
readonly input: Record<string, unknown>;
|
|
16
|
+
readonly options: {
|
|
17
|
+
readonly codemode: false;
|
|
18
|
+
};
|
|
19
|
+
readonly execute: (input: unknown, context: ToolCallContextLike) => Promise<ToolResultLike>;
|
|
20
|
+
};
|
|
21
|
+
export type ToolEditorLike = {
|
|
22
|
+
add(tool: ToolInfoLike): void;
|
|
23
|
+
};
|
|
24
|
+
export declare function adaptTool(name: string, definition: ToolDefinition, projectRoot: string): ToolInfoLike;
|
|
25
|
+
export declare function litToolsForProject(projectRoot: string): readonly ToolInfoLike[];
|
|
26
|
+
export declare function addLitTools(editor: ToolEditorLike, tools: readonly ToolInfoLike[]): void;
|
package/dist/v2/tools.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// LitOpenCode's five tools on OpenCode 2. The tool bodies are the OpenCode 1 ones; this adapter gives
|
|
2
|
+
// each a closed JSON Schema input, validates the call before it runs, hands the body an OpenCode 1
|
|
3
|
+
// style context whose directory is the plugin's project, and returns `{ content, metadata }`. Each tool
|
|
4
|
+
// sits on the model's own tool list (`codemode: false`), as it did on OpenCode 1, so lit-plan, which has
|
|
5
|
+
// no Code Mode `execute`, still has them.
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { litOpenCodeTools } from "../tools.js";
|
|
8
|
+
function resultToV2(result) {
|
|
9
|
+
if (typeof result === "string")
|
|
10
|
+
return { content: result };
|
|
11
|
+
return {
|
|
12
|
+
content: result.output,
|
|
13
|
+
metadata: { ...(result.metadata ?? {}), ...(result.title === undefined ? {} : { title: result.title }) }
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function adaptTool(name, definition, projectRoot) {
|
|
17
|
+
const schema = z.strictObject(definition.args);
|
|
18
|
+
const { $schema: _dialect, ...input } = z.toJSONSchema(schema, { io: "input" });
|
|
19
|
+
return {
|
|
20
|
+
name,
|
|
21
|
+
description: definition.description,
|
|
22
|
+
input,
|
|
23
|
+
options: { codemode: false },
|
|
24
|
+
async execute(raw, context) {
|
|
25
|
+
const args = schema.parse(raw ?? {});
|
|
26
|
+
const v1Context = {
|
|
27
|
+
sessionID: context.sessionID,
|
|
28
|
+
messageID: context.messageID,
|
|
29
|
+
agent: context.agent,
|
|
30
|
+
directory: projectRoot,
|
|
31
|
+
worktree: projectRoot,
|
|
32
|
+
abort: context.signal,
|
|
33
|
+
metadata: () => undefined,
|
|
34
|
+
// No tool body asks today; a future one must not be approved without the user seeing a prompt.
|
|
35
|
+
ask: async () => {
|
|
36
|
+
throw new Error(`${name}: asking for permission from inside a tool is not supported on OpenCode 2`);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
return resultToV2(await definition.execute(args, v1Context));
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function litToolsForProject(projectRoot) {
|
|
44
|
+
return Object.entries(litOpenCodeTools).map(([name, definition]) => adaptTool(name, definition, projectRoot));
|
|
45
|
+
}
|
|
46
|
+
export function addLitTools(editor, tools) {
|
|
47
|
+
for (const tool of tools)
|
|
48
|
+
editor.add(tool);
|
|
49
|
+
}
|
package/dist/v2/tui.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type MissionDataV2 } from "../tui/mission-source-v2.ts";
|
|
2
|
+
import { type MissionPalette } from "../tui/mission-text.ts";
|
|
3
|
+
export type TuiRuntime = {
|
|
4
|
+
readonly jsx: (type: string, props?: Record<string, unknown>) => unknown;
|
|
5
|
+
readonly createSignal: <T>(value: T) => readonly [() => T, (value: T) => T];
|
|
6
|
+
readonly createEffect: (fn: () => void) => void;
|
|
7
|
+
readonly createRoot: <T>(fn: (dispose: () => void) => T) => T;
|
|
8
|
+
};
|
|
9
|
+
type PromptFooterInput = {
|
|
10
|
+
readonly sessionID?: string;
|
|
11
|
+
readonly mode: "normal" | "shell";
|
|
12
|
+
readonly showDetails: boolean;
|
|
13
|
+
};
|
|
14
|
+
export type SlotClaimLike = {
|
|
15
|
+
readonly append: "sidebar.content";
|
|
16
|
+
readonly render: (input: {
|
|
17
|
+
readonly sessionID: string;
|
|
18
|
+
}) => unknown;
|
|
19
|
+
} | {
|
|
20
|
+
readonly append: "prompt.footer.status";
|
|
21
|
+
readonly render: (input: PromptFooterInput) => unknown;
|
|
22
|
+
};
|
|
23
|
+
export type TuiContextLike = {
|
|
24
|
+
readonly location: {
|
|
25
|
+
readonly directory: string;
|
|
26
|
+
} | undefined;
|
|
27
|
+
readonly theme: unknown;
|
|
28
|
+
readonly themeMode: "dark" | "light";
|
|
29
|
+
readonly ui: {
|
|
30
|
+
readonly slot: (claim: SlotClaimLike) => () => void;
|
|
31
|
+
readonly toast: {
|
|
32
|
+
readonly show: (options: {
|
|
33
|
+
readonly title?: string;
|
|
34
|
+
readonly message: string;
|
|
35
|
+
readonly variant?: "info" | "success" | "warning" | "error";
|
|
36
|
+
readonly duration?: number;
|
|
37
|
+
readonly sessionID?: string;
|
|
38
|
+
}) => void;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
readonly storage: {
|
|
42
|
+
readonly store: <Value extends object>(key: string, options: {
|
|
43
|
+
readonly initial: Value;
|
|
44
|
+
}) => readonly [Value, unknown];
|
|
45
|
+
};
|
|
46
|
+
readonly data: MissionDataV2 & {
|
|
47
|
+
readonly listen: (handler: (event: {
|
|
48
|
+
readonly details: {
|
|
49
|
+
readonly type: string;
|
|
50
|
+
};
|
|
51
|
+
}) => void) => () => void;
|
|
52
|
+
readonly location: {
|
|
53
|
+
default(): {
|
|
54
|
+
readonly directory: string;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
/** OpenCode 2 theme tokens under the names the mission palette reads. */
|
|
60
|
+
export declare function paletteFromV2Theme(theme: unknown, mode: "dark" | "light"): MissionPalette;
|
|
61
|
+
export declare function ignitionLine(name: string): string;
|
|
62
|
+
export declare function setupLitOpenCodeTui(context: TuiContextLike, runtime?: TuiRuntime): Promise<() => Promise<void>>;
|
|
63
|
+
export {};
|