pi-weave 0.2.4 → 0.3.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.
- package/README.md +40 -21
- package/package.json +12 -2
- package/skills/weave-explore/SKILL.md +17 -33
- package/skills/weave-notepad/SKILL.md +57 -120
- package/skills/weave-notepad/references/link-repair.md +25 -73
- package/src/core/index.ts +25 -1
- package/src/core/noteAction.ts +343 -0
- package/src/core/repoAction.ts +84 -0
- package/src/core/sessions.ts +80 -1
- package/src/core/summaries.ts +16 -0
- package/src/core/workspace.ts +1 -1
- package/src/core/workspaceCommands.ts +227 -0
- package/src/opencode/commands.ts +138 -0
- package/src/opencode/index.ts +88 -0
- package/src/opencode/rpc.ts +37 -0
- package/src/opencode/tools.ts +43 -0
- package/src/opencode/tui.ts +96 -0
- package/src/opencode/v1.ts +161 -0
- package/src/pi/sessionScan.ts +4 -28
- package/src/pi/summarize.ts +10 -25
- package/src/pi/tools/noteTool.ts +7 -406
- package/src/pi/tools/repoTool.ts +8 -75
- package/src/pi/viewer/web/run.ts +16 -77
- package/src/web/client/dist/app.js +13 -13
- package/src/web/client/graph/graph.model.ts +6 -4
- package/src/web/server/controller.ts +79 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import type { Plugin, ToolDefinition } from "@opencode-ai/plugin";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import type { SessionMessagesResponse } from "@opencode-ai/sdk";
|
|
5
|
+
import { hashContent } from "../core";
|
|
6
|
+
import { createWorkspaceCommands } from "../core/workspaceCommands";
|
|
7
|
+
import { WorkspaceServerController } from "../web/server/controller";
|
|
8
|
+
import { openCodeSessionDigest } from "./commands";
|
|
9
|
+
import { weaveTools } from "./tools";
|
|
10
|
+
|
|
11
|
+
type Client = Parameters<Plugin>[0]["client"];
|
|
12
|
+
type CommandResult = { lines: string[]; viewer?: { url: string; open: boolean } };
|
|
13
|
+
|
|
14
|
+
// ponytail: only our flat tool schemas; use a converter if nested inputs are added.
|
|
15
|
+
function legacyArgs(input: (typeof weaveTools)[number]["input"]) {
|
|
16
|
+
return Object.fromEntries(Object.entries(input.properties).map(([name, definition]) => {
|
|
17
|
+
const schema = "enum" in definition ? z.enum(definition.enum)
|
|
18
|
+
: definition.type === "array" ? z.array(z.string())
|
|
19
|
+
: definition.type === "boolean" ? z.boolean()
|
|
20
|
+
: definition.type === "number" ? z.number()
|
|
21
|
+
: z.string();
|
|
22
|
+
const described = "description" in definition ? schema.describe(definition.description) : schema;
|
|
23
|
+
return [name, (input.required as readonly string[]).includes(name) ? described : described.optional()];
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function modelOf(messages: SessionMessagesResponse) {
|
|
28
|
+
for (const { info, parts } of [...messages].reverse()) {
|
|
29
|
+
if (info.role === "user" && !parts.some((part) => part.type === "text" && part.metadata?.["pi-weave"])) return info.model;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function v1SessionDigest(session: { id: string; directory: string }, messages: SessionMessagesResponse) {
|
|
35
|
+
return openCodeSessionDigest({ ...session, location: { directory: session.directory } }, messages.map(({ info, parts }) => {
|
|
36
|
+
const text = parts.filter((part) => part.type === "text" && !part.synthetic && !part.ignored)
|
|
37
|
+
.map((part) => part.type === "text" ? part.text : "").join("\n");
|
|
38
|
+
if (info.role === "user") return { type: "user", text };
|
|
39
|
+
return {
|
|
40
|
+
type: "assistant",
|
|
41
|
+
model: { providerID: info.providerID, id: info.modelID },
|
|
42
|
+
error: info.error,
|
|
43
|
+
content: parts.map((part) => part.type === "tool" ? { ...part, name: part.tool } : part),
|
|
44
|
+
};
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function generate(client: Client, sessionID: string, prompt: string, signal: AbortSignal, selectedModel: string) {
|
|
49
|
+
const separator = selectedModel.indexOf("/");
|
|
50
|
+
const model = { providerID: selectedModel.slice(0, separator), modelID: selectedModel.slice(separator + 1) };
|
|
51
|
+
const { data: session } = await client.session.get({ path: { id: sessionID }, signal, throwOnError: true });
|
|
52
|
+
// The V1 SDK's legacy types omit permission, but 1.18.29's session API accepts it.
|
|
53
|
+
const body = { parentID: sessionID, title: "pi-weave scan", permission: [{ permission: "*", pattern: "*", action: "deny" }] };
|
|
54
|
+
const query = { directory: session.directory };
|
|
55
|
+
const { data: child } = await client.session.create({ body, query, throwOnError: true });
|
|
56
|
+
const path = { id: child.id };
|
|
57
|
+
let aborting: Promise<unknown> | undefined;
|
|
58
|
+
const abort = () => { aborting = client.session.abort({ path, query, throwOnError: true }).catch(() => {}); };
|
|
59
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
60
|
+
try {
|
|
61
|
+
signal.throwIfAborted();
|
|
62
|
+
const { data } = await client.session.prompt({
|
|
63
|
+
path, query,
|
|
64
|
+
body: { model, parts: [{ type: "text", text: prompt }] },
|
|
65
|
+
signal,
|
|
66
|
+
throwOnError: true,
|
|
67
|
+
});
|
|
68
|
+
if (data.info.error) throw new Error(JSON.stringify(data.info.error));
|
|
69
|
+
return data.parts.filter((part) => part.type === "text").map((part) => part.text).join("\n");
|
|
70
|
+
} finally {
|
|
71
|
+
signal.removeEventListener("abort", abort);
|
|
72
|
+
await aborting;
|
|
73
|
+
await client.session.delete({ path, query, throwOnError: true });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const server: Plugin = async ({ client }) => {
|
|
78
|
+
const viewer = new WorkspaceServerController();
|
|
79
|
+
const collected = new Map<string, CommandResult>();
|
|
80
|
+
const toast = (message: string, variant: "info" | "success" | "error" = "info") =>
|
|
81
|
+
client.tui.showToast({ body: { title: "pi-weave", message, variant } }).catch(() => {});
|
|
82
|
+
let lastProgress = 0;
|
|
83
|
+
const shared = createWorkspaceCommands({
|
|
84
|
+
async getSession(sessionID) {
|
|
85
|
+
const { data: session } = await client.session.get({ path: { id: sessionID }, throwOnError: true });
|
|
86
|
+
const { data: messages } = await client.session.messages({ path: { id: sessionID }, throwOnError: true });
|
|
87
|
+
const model = modelOf(messages);
|
|
88
|
+
return { id: session.id, cwd: session.directory, model: model ? `${model.providerID}/${model.modelID}` : null };
|
|
89
|
+
},
|
|
90
|
+
async getDigest(sessionID, signal) {
|
|
91
|
+
const { data: session } = await client.session.get({ path: { id: sessionID }, signal, throwOnError: true });
|
|
92
|
+
const { data: messages } = await client.session.messages({ path: { id: sessionID }, signal, throwOnError: true });
|
|
93
|
+
const relevant = messages.filter(({ parts }) => !parts.some((part) => part.type === "text" && part.metadata?.["pi-weave"]));
|
|
94
|
+
return { digest: v1SessionDigest(session, relevant), hash: hashContent(JSON.stringify(relevant)) };
|
|
95
|
+
},
|
|
96
|
+
sessionSource: (id) => `opencode:session:${id}`,
|
|
97
|
+
generate: (id, prompt, signal, model) => generate(client, id, prompt, signal, model),
|
|
98
|
+
async output(sessionID, text) {
|
|
99
|
+
const output = collected.get(sessionID);
|
|
100
|
+
if (output) { output.lines.push(text); return; }
|
|
101
|
+
await client.session.prompt({
|
|
102
|
+
path: { id: sessionID },
|
|
103
|
+
body: { noReply: true, parts: [{ type: "text", text, synthetic: true, metadata: { "pi-weave": true } }] },
|
|
104
|
+
throwOnError: true,
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
}, (status) => {
|
|
108
|
+
if (status.active && Date.now() - lastProgress < 2_000) return;
|
|
109
|
+
lastProgress = Date.now();
|
|
110
|
+
void toast(status.text, status.active ? "info" : status.text.includes("failed") ? "error" : "success");
|
|
111
|
+
}, viewer, ({ sessionID, url, open }) => {
|
|
112
|
+
const result = collected.get(sessionID);
|
|
113
|
+
if (result) result.viewer = { url, open };
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
tool: Object.fromEntries(weaveTools.map((definition) => [definition.name, {
|
|
118
|
+
description: definition.description,
|
|
119
|
+
args: legacyArgs(definition.input),
|
|
120
|
+
async execute(input, context) {
|
|
121
|
+
const result = await definition.execute(input, context.directory, async (title) => context.metadata({ title }));
|
|
122
|
+
return { output: result.text, metadata: result.details };
|
|
123
|
+
},
|
|
124
|
+
} satisfies ToolDefinition])),
|
|
125
|
+
async config(config) {
|
|
126
|
+
// V1's public runtime supports skills.paths; its legacy SDK Config omits it.
|
|
127
|
+
const extended = config as typeof config & { skills?: { paths?: string[] } };
|
|
128
|
+
extended.skills ??= {};
|
|
129
|
+
extended.skills.paths = [...new Set([...(extended.skills.paths ?? []), fileURLToPath(new URL("../../skills", import.meta.url))])];
|
|
130
|
+
config.command ??= {};
|
|
131
|
+
for (const command of shared.commands) config.command[command.name] = {
|
|
132
|
+
description: command.description,
|
|
133
|
+
template: "Report the pi-weave command result below. Do not run additional tools. $ARGUMENTS",
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
async "command.execute.before"(input, output) {
|
|
137
|
+
const command = shared.commands.find((command) => command.name === input.command);
|
|
138
|
+
if (!command) return;
|
|
139
|
+
const result: CommandResult = { lines: [] };
|
|
140
|
+
collected.set(input.sessionID, result);
|
|
141
|
+
try {
|
|
142
|
+
// V1 cannot suppress the normal model reply after this hook.
|
|
143
|
+
await command.execute(input.sessionID, input.arguments);
|
|
144
|
+
const text = result.lines.join("\n") || "Scan started; completion will be reported separately.";
|
|
145
|
+
// V1 retains the original parts array after this hook; mutate it in place.
|
|
146
|
+
for (const part of output.parts) {
|
|
147
|
+
if (part.type !== "text") continue;
|
|
148
|
+
part.text = `Report this pi-weave result without running tools:\n${text}`;
|
|
149
|
+
part.synthetic = true;
|
|
150
|
+
part.metadata = { "pi-weave": true, "pi-weave-result": { text, viewer: result.viewer } };
|
|
151
|
+
}
|
|
152
|
+
} finally {
|
|
153
|
+
collected.delete(input.sessionID);
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
async dispose() {
|
|
157
|
+
await shared.cleanup();
|
|
158
|
+
await viewer.close();
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
};
|
package/src/pi/sessionScan.ts
CHANGED
|
@@ -14,21 +14,14 @@ import {
|
|
|
14
14
|
resolveSessionsRoot,
|
|
15
15
|
resolveVaultRoot,
|
|
16
16
|
runSessionScan,
|
|
17
|
+
SESSION_SCAN_SYSTEM_PROMPT,
|
|
18
|
+
formatSessionScanResult,
|
|
17
19
|
type SessionScanOptions,
|
|
18
20
|
type SessionScanResult,
|
|
19
21
|
type SummarizeFn,
|
|
20
22
|
} from "../core";
|
|
21
23
|
import { createModelSummarizer, type SummarizerDeps } from "./summarize";
|
|
22
24
|
|
|
23
|
-
const SESSION_SYSTEM_PROMPT = [
|
|
24
|
-
"You write durable memory notes that compact coding-agent sessions into their bottom line.",
|
|
25
|
-
"First summarize what happened, what shipped (features, files, commands, decisions), and",
|
|
26
|
-
"what went less well (dead ends, breakage, unfinished work) using concrete technical details.",
|
|
27
|
-
"Then add a '## Takeaways' section with 2–4 reusable lessons: gotchas, root causes of tricky",
|
|
28
|
-
"failures, non-obvious syntax rules, or architecture patterns a future agent can apply.",
|
|
29
|
-
"Past tense. No preamble or code fences. Omit lessons unsupported by the transcript.",
|
|
30
|
-
].join("\n");
|
|
31
|
-
|
|
32
25
|
const SESSION_MAX_OUTPUT_TOKENS = 3_000;
|
|
33
26
|
|
|
34
27
|
/** Session summarizer over the session model, or null when none is active. */
|
|
@@ -36,7 +29,7 @@ export function createSessionSummarizer(
|
|
|
36
29
|
ctx: Pick<ExtensionContext, "model" | "modelRegistry">,
|
|
37
30
|
deps: SummarizerDeps = {},
|
|
38
31
|
): { summarize: SummarizeFn; label: string } | null {
|
|
39
|
-
return createModelSummarizer(ctx,
|
|
32
|
+
return createModelSummarizer(ctx, SESSION_SCAN_SYSTEM_PROMPT, SESSION_MAX_OUTPUT_TOKENS, deps);
|
|
40
33
|
}
|
|
41
34
|
|
|
42
35
|
export type SessionScanOutcome =
|
|
@@ -85,21 +78,4 @@ export async function scanPiSessions(
|
|
|
85
78
|
return { kind: "ok", result };
|
|
86
79
|
}
|
|
87
80
|
|
|
88
|
-
|
|
89
|
-
export function formatSessionScanResult(result: SessionScanResult): string {
|
|
90
|
-
const parts = [
|
|
91
|
-
`${result.written} summarized (${result.created} new, ${result.updated} updated)`,
|
|
92
|
-
`${result.skippedFresh} unchanged`,
|
|
93
|
-
];
|
|
94
|
-
if (result.skippedEmpty > 0) parts.push(`${result.skippedEmpty} empty`);
|
|
95
|
-
if (result.skippedTooBig > 0) parts.push(`${result.skippedTooBig} skipped (size)`);
|
|
96
|
-
if (result.skippedUnreadable > 0) parts.push(`${result.skippedUnreadable} unreadable`);
|
|
97
|
-
let text = `${parts.join(", ")} — ${result.considered} sessions considered`;
|
|
98
|
-
if (result.failed.length > 0) {
|
|
99
|
-
const [failed0] = result.failed;
|
|
100
|
-
if (failed0) {
|
|
101
|
-
text += `; ${result.failed.length} failed, first: ${failed0.path}: ${failed0.error}`;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return text;
|
|
105
|
-
}
|
|
81
|
+
export { formatSessionScanResult };
|
package/src/pi/summarize.ts
CHANGED
|
@@ -17,7 +17,14 @@ import {
|
|
|
17
17
|
type Model,
|
|
18
18
|
} from "@earendil-works/pi-ai";
|
|
19
19
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
DEEP_SCAN_SYSTEM_PROMPT,
|
|
22
|
+
formatDeepScanResult,
|
|
23
|
+
runDeepScan,
|
|
24
|
+
type DeepScanOptions,
|
|
25
|
+
type DeepScanResult,
|
|
26
|
+
type SummarizeFn,
|
|
27
|
+
} from "../core";
|
|
21
28
|
|
|
22
29
|
export type CompleteFn = (
|
|
23
30
|
model: Model<Api>,
|
|
@@ -35,12 +42,6 @@ export interface LlmSummarizer {
|
|
|
35
42
|
label: string;
|
|
36
43
|
}
|
|
37
44
|
|
|
38
|
-
const SYSTEM_PROMPT = [
|
|
39
|
-
"You write terse, navigation-oriented summaries of source files for a codebase index.",
|
|
40
|
-
"Rules: 1–3 sentences. What the file does, its outward surface (exports/routes/commands),",
|
|
41
|
-
"anything surprising (globals, side effects, generated sections). No preamble, no headings, no code fences.",
|
|
42
|
-
].join("\n");
|
|
43
|
-
|
|
44
45
|
const MAX_OUTPUT_TOKENS = 220;
|
|
45
46
|
const REQUEST_TIMEOUT_MS = 30_000;
|
|
46
47
|
|
|
@@ -138,7 +139,7 @@ export function createLlmSummarizer(
|
|
|
138
139
|
ctx: Pick<ExtensionContext, "model" | "modelRegistry">,
|
|
139
140
|
deps: SummarizerDeps = {},
|
|
140
141
|
): LlmSummarizer | null {
|
|
141
|
-
return createModelSummarizer(ctx,
|
|
142
|
+
return createModelSummarizer(ctx, DEEP_SCAN_SYSTEM_PROMPT, MAX_OUTPUT_TOKENS, deps);
|
|
142
143
|
}
|
|
143
144
|
|
|
144
145
|
export type DeepScanOutcome =
|
|
@@ -178,20 +179,4 @@ export async function deepScanRepository(
|
|
|
178
179
|
return { kind: "ok", result };
|
|
179
180
|
}
|
|
180
181
|
|
|
181
|
-
|
|
182
|
-
export function formatDeepScanResult(result: DeepScanResult): string {
|
|
183
|
-
const parts = [
|
|
184
|
-
`${result.written} summarized`,
|
|
185
|
-
`${result.skippedFresh} unchanged`,
|
|
186
|
-
];
|
|
187
|
-
if (result.skippedTooBig > 0) parts.push(`${result.skippedTooBig} skipped (size/type)`);
|
|
188
|
-
if (result.pruned > 0) parts.push(`${result.pruned} pruned`);
|
|
189
|
-
let text = `${parts.join(", ")} — ${result.considered} files considered`;
|
|
190
|
-
if (result.failed.length > 0) {
|
|
191
|
-
const [failed0] = result.failed;
|
|
192
|
-
if (failed0) {
|
|
193
|
-
text += `; ${result.failed.length} failed, first: ${failed0.path}: ${failed0.error}`;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
return text;
|
|
197
|
-
}
|
|
182
|
+
export { formatDeepScanResult };
|