@evandu/super-pi 0.1.110 → 0.1.112

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 (37) hide show
  1. package/dist/.super-pi-check-inputs.json +2 -2
  2. package/dist/client/.super-pi-client-sources.json +9 -7
  3. package/dist/client/assets/{CodeMirrorEditor-DdNOmxYW.js → CodeMirrorEditor-moFTk5SE.js} +2 -2
  4. package/dist/client/assets/{dist-BcnfVRW2.js → dist-BR4s0dNg.js} +1 -1
  5. package/dist/client/assets/{dist-D_JtGl0X.js → dist-BUuPRG15.js} +1 -1
  6. package/dist/client/assets/{dist-BaXshVGE.js → dist-Bds_PUYK.js} +1 -1
  7. package/dist/client/assets/{dist-D6LObgqS.js → dist-BsO40qxQ.js} +1 -1
  8. package/dist/client/assets/{dist-DFYECMpZ.js → dist-CCYnr6ul.js} +1 -1
  9. package/dist/client/assets/{dist-VljXHWNt.js → dist-CL65aMXY.js} +1 -1
  10. package/dist/client/assets/{dist-CmNELnsL.js → dist-D9m04q-s.js} +1 -1
  11. package/dist/client/assets/{dist-CUsER3MR.js → dist-DIfmMr8s.js} +1 -1
  12. package/dist/client/assets/{dist-r4nQMwvT.js → dist-DPDGd2jl.js} +1 -1
  13. package/dist/client/assets/{dist-BMx01Qs-.js → dist-DPvy57Ph.js} +1 -1
  14. package/dist/client/assets/{dist-B0owH_FO.js → dist-DQjJMys5.js} +1 -1
  15. package/dist/client/assets/{dist-DfUFnKum.js → dist-Dnr5BxAi.js} +1 -1
  16. package/dist/client/assets/{dist-DyYBaVQ5.js → dist-DrbUrVWB.js} +1 -1
  17. package/dist/client/assets/{dist-CyuGJahk.js → dist-DuWhzonJ.js} +1 -1
  18. package/dist/client/assets/{dist-CiFn8_Gb.js → dist-TgNJUr7p.js} +1 -1
  19. package/dist/client/assets/{dist-CB4FCjuA.js → dist-rhPxFuD1.js} +1 -1
  20. package/dist/client/assets/{index-DmN1fZRn.js → index-BixdV7aQ.js} +23 -23
  21. package/dist/client/assets/index-CGXMw9LP.css +1 -0
  22. package/dist/client/index.html +2 -2
  23. package/dist/server/application/run-service.d.ts +7 -0
  24. package/dist/server/application/run-service.js +30 -0
  25. package/dist/server/file-name-index.d.ts +144 -0
  26. package/dist/server/file-name-index.js +324 -0
  27. package/dist/server/http-server.js +112 -3
  28. package/dist/server/index.js +10 -0
  29. package/dist/server/server-context.d.ts +9 -0
  30. package/dist/server/skills.d.ts +23 -0
  31. package/dist/server/skills.js +171 -0
  32. package/dist/server/store.d.ts +9 -0
  33. package/dist/server/store.js +11 -0
  34. package/dist/server/types.d.ts +7 -0
  35. package/package.json +4 -1
  36. package/scripts/patch-brace-expansion.mjs +1 -1
  37. package/dist/client/assets/index-CGlVRKg9.css +0 -1
@@ -0,0 +1,23 @@
1
+ import type { SkillSummary } from "./types.js";
2
+ import type { ActiveMemory } from "./active-memory.js";
3
+ /**
4
+ * Loads and returns all available skills for a given workspace and global environment.
5
+ * Priority is given to Pi SDK's DefaultResourceLoader which scans project and user skills.
6
+ * Falls back to direct filesystem scanning if Pi SDK is unavailable.
7
+ */
8
+ export declare function getAvailableSkills(workspacePath?: string, options?: {
9
+ memory?: ActiveMemory;
10
+ workspaceId?: string;
11
+ }): Promise<SkillSummary[]>;
12
+ /**
13
+ * Reads the markdown content of a specific skill by name or ID.
14
+ */
15
+ export declare function readSkill(nameOrId: string, workspacePath?: string, options?: {
16
+ memory?: ActiveMemory;
17
+ workspaceId?: string;
18
+ }): Promise<{
19
+ id: string;
20
+ name: string;
21
+ description: string;
22
+ content: string;
23
+ } | null>;
@@ -0,0 +1,171 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readdir, readFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ /**
6
+ * Loads and returns all available skills for a given workspace and global environment.
7
+ * Priority is given to Pi SDK's DefaultResourceLoader which scans project and user skills.
8
+ * Falls back to direct filesystem scanning if Pi SDK is unavailable.
9
+ */
10
+ export async function getAvailableSkills(workspacePath, options) {
11
+ const cwd = workspacePath && existsSync(workspacePath) ? workspacePath : process.cwd();
12
+ const skillsMap = new Map();
13
+ // 1. Try loading via Pi SDK DefaultResourceLoader
14
+ try {
15
+ const sdk = await import("@earendil-works/pi-coding-agent").catch(() => null);
16
+ if (sdk?.DefaultResourceLoader) {
17
+ const agentDir = join(homedir(), ".pi", "agent");
18
+ const loader = new sdk.DefaultResourceLoader({ cwd, agentDir });
19
+ await loader.reload();
20
+ const res = loader.getSkills();
21
+ if (Array.isArray(res?.skills)) {
22
+ for (const s of res.skills) {
23
+ if (!s.name)
24
+ continue;
25
+ const scope = s.sourceInfo?.scope === "project" ? "workspace" : "global";
26
+ skillsMap.set(s.name, {
27
+ id: `skill:${s.name}`,
28
+ name: s.name,
29
+ description: s.description || "",
30
+ scope,
31
+ filePath: s.filePath,
32
+ });
33
+ }
34
+ }
35
+ }
36
+ }
37
+ catch {
38
+ // Ignore and proceed to filesystem fallback
39
+ }
40
+ // 2. Fallback: Direct directory scan if SDK loaded nothing or was unavailable
41
+ if (skillsMap.size === 0) {
42
+ const scanDirs = [
43
+ { dir: join(cwd, ".pi", "skills"), scope: "workspace" },
44
+ { dir: join(cwd, ".agents", "skills"), scope: "workspace" },
45
+ { dir: join(homedir(), ".pi", "agent", "skills"), scope: "global" },
46
+ { dir: join(homedir(), ".agents", "skills"), scope: "global" },
47
+ ];
48
+ for (const { dir, scope } of scanDirs) {
49
+ if (!existsSync(dir))
50
+ continue;
51
+ try {
52
+ const entries = await readdir(dir, { withFileTypes: true });
53
+ for (const entry of entries) {
54
+ let skillFile = "";
55
+ let skillName = entry.name;
56
+ if (entry.isDirectory()) {
57
+ const candidate = join(dir, entry.name, "SKILL.md");
58
+ if (existsSync(candidate)) {
59
+ skillFile = candidate;
60
+ skillName = entry.name;
61
+ }
62
+ }
63
+ else if (entry.isFile() && entry.name.endsWith(".md")) {
64
+ skillFile = join(dir, entry.name);
65
+ skillName = entry.name.replace(/\.md$/, "");
66
+ }
67
+ if (skillFile) {
68
+ try {
69
+ const text = await readFile(skillFile, "utf-8");
70
+ const frontmatterMatch = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
71
+ let description = "";
72
+ let parsedName = skillName;
73
+ if (frontmatterMatch) {
74
+ const nameMatch = /^name:\s*(.+)$/m.exec(frontmatterMatch[1]);
75
+ if (nameMatch)
76
+ parsedName = nameMatch[1].trim();
77
+ const descMatch = /^description:\s*(.+)$/m.exec(frontmatterMatch[1]);
78
+ if (descMatch)
79
+ description = descMatch[1].trim();
80
+ }
81
+ if (!skillsMap.has(parsedName)) {
82
+ skillsMap.set(parsedName, {
83
+ id: `skill:${parsedName}`,
84
+ name: parsedName,
85
+ description,
86
+ scope,
87
+ filePath: skillFile,
88
+ });
89
+ }
90
+ }
91
+ catch {
92
+ // Ignore unreadable file
93
+ }
94
+ }
95
+ }
96
+ }
97
+ catch {
98
+ // Ignore unreadable directory
99
+ }
100
+ }
101
+ }
102
+ // 3. Optional: merge active memory notes skills if available and not colliding
103
+ if (options?.memory && options?.workspaceId) {
104
+ try {
105
+ const memoryEntries = await options.memory.notes.apply(options.workspaceId, "", { target: "skills", action: "list" });
106
+ if (Array.isArray(memoryEntries)) {
107
+ for (const entry of memoryEntries) {
108
+ const firstLine = entry.content.split(/\r?\n/, 1)[0].replace(/^#+\s*/, "").trim();
109
+ const name = firstLine || "unnamed-skill";
110
+ if (!skillsMap.has(name) && !skillsMap.has(entry.id)) {
111
+ skillsMap.set(`mem:${entry.id}`, {
112
+ id: `skill:${entry.id}`,
113
+ name,
114
+ description: "Active memory skill",
115
+ scope: "memory",
116
+ });
117
+ }
118
+ }
119
+ }
120
+ }
121
+ catch {
122
+ // Memory may not be ready or active
123
+ }
124
+ }
125
+ return Array.from(skillsMap.values()).sort((a, b) => a.name.localeCompare(b.name));
126
+ }
127
+ /**
128
+ * Reads the markdown content of a specific skill by name or ID.
129
+ */
130
+ export async function readSkill(nameOrId, workspacePath, options) {
131
+ const cleanId = nameOrId.startsWith("skill:") ? nameOrId.slice("skill:".length) : nameOrId;
132
+ const skills = await getAvailableSkills(workspacePath, options);
133
+ const found = skills.find((s) => s.name === cleanId || s.name === nameOrId || s.id === nameOrId || s.id === `skill:${cleanId}`);
134
+ if (!found)
135
+ return null;
136
+ // If it's a file on disk
137
+ if (found.filePath && existsSync(found.filePath)) {
138
+ try {
139
+ const content = await readFile(found.filePath, "utf-8");
140
+ return {
141
+ id: found.id,
142
+ name: found.name,
143
+ description: found.description,
144
+ content,
145
+ };
146
+ }
147
+ catch {
148
+ return null;
149
+ }
150
+ }
151
+ // If it's stored in active memory
152
+ if (found.scope === "memory" && options?.memory && options?.workspaceId) {
153
+ try {
154
+ const entryId = found.id.startsWith("skill:") ? found.id.slice("skill:".length) : found.id;
155
+ const memoryEntries = await options.memory.notes.apply(options.workspaceId, "", { target: "skills", action: "list" });
156
+ const entry = memoryEntries.find((e) => e.id === entryId || e.id === found.name);
157
+ if (entry) {
158
+ return {
159
+ id: found.id,
160
+ name: found.name,
161
+ description: found.description,
162
+ content: entry.content,
163
+ };
164
+ }
165
+ }
166
+ catch {
167
+ return null;
168
+ }
169
+ }
170
+ return null;
171
+ }
@@ -91,6 +91,15 @@ export declare class Store {
91
91
  */
92
92
  setDefaultModel(model?: ModelSelection): Promise<void>;
93
93
  snapshot(): AppState;
94
+ /**
95
+ * Whether a conversation already holds a message with this id.
96
+ *
97
+ * A LIVE read with no clone, because its one caller is the send path's duplicate check: a resend
98
+ * that arrives within milliseconds of the original must be answered without paying for a full
99
+ * copy of every conversation in the app. The answer is a boolean and the message is not handed
100
+ * out, so nothing can mutate the store through it.
101
+ */
102
+ hasMessage(conversationId: string, messageId: string): boolean;
94
103
  /**
95
104
  * One conversation and its workspace, as DETACHED copies, without cloning the whole state.
96
105
  *
@@ -684,6 +684,17 @@ export class Store {
684
684
  }
685
685
  }
686
686
  snapshot() { return structuredClone(this.state); }
687
+ /**
688
+ * Whether a conversation already holds a message with this id.
689
+ *
690
+ * A LIVE read with no clone, because its one caller is the send path's duplicate check: a resend
691
+ * that arrives within milliseconds of the original must be answered without paying for a full
692
+ * copy of every conversation in the app. The answer is a boolean and the message is not handed
693
+ * out, so nothing can mutate the store through it.
694
+ */
695
+ hasMessage(conversationId, messageId) {
696
+ return Boolean(this.state.conversations.find((item) => item.id === conversationId)?.messages.some((message) => message.id === messageId));
697
+ }
687
698
  /**
688
699
  * One conversation and its workspace, as DETACHED copies, without cloning the whole state.
689
700
  *
@@ -164,6 +164,13 @@ export type MessageAttachment = {
164
164
  content: string;
165
165
  };
166
166
  export type MessageAttachments = MessageAttachment[];
167
+ export interface SkillSummary {
168
+ id: string;
169
+ name: string;
170
+ description: string;
171
+ scope?: "workspace" | "global" | "memory";
172
+ filePath?: string;
173
+ }
167
174
  /**
168
175
  * ONE PARAMETER OF A TOOL CALL, as a call block lists it, and the boundary that keeps Pi's arguments out
169
176
  * of `state.json`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evandu/super-pi",
3
- "version": "0.1.110",
3
+ "version": "0.1.112",
4
4
  "description": "Web control plane and companion for Pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -89,12 +89,14 @@
89
89
  "scripts/files-missing-path.test.mjs",
90
90
  "scripts/file-changes.test.mjs",
91
91
  "scripts/file-index.test.mjs",
92
+ "scripts/file-name-index.test.mjs",
92
93
  "scripts/file-content-search.test.mjs",
93
94
  "scripts/file-diff-client.test.mjs",
94
95
  "scripts/task-tool.test.mjs",
95
96
  "scripts/tool-register-env.test.mjs",
96
97
  "scripts/bg-task-zombie.test.mjs",
97
98
  "scripts/scoped-sidebar-tabs.test.mjs",
99
+ "scripts/send-retry.test.mjs",
98
100
  "scripts/git-diff-view.test.mjs",
99
101
  "scripts/fork-server.test.mjs",
100
102
  "scripts/styles-no-dangling.test.mjs",
@@ -115,6 +117,7 @@
115
117
  "scripts/static-client-assets.test.mjs",
116
118
  "scripts/cli.test.mjs",
117
119
  "scripts/check-runner.test.mjs",
120
+ "scripts/skills-discovery.test.mjs",
118
121
  "scripts/shared-chrome.test.mjs"
119
122
  ],
120
123
  "check:browser": [
@@ -220,7 +220,7 @@ function findVulnerableCopies() {
220
220
  try { version = JSON.parse(readFileSync(manifest, "utf8")).version ?? ""; } catch { continue; }
221
221
  if (!version) continue;
222
222
  if (compareVersions(version, VULNERABLE_BELOW) >= 0) continue;
223
- found.push({ directory, version, lockKey: lockKeyOf(directory) });
223
+ found.push({ directory, version });
224
224
  }
225
225
  return found;
226
226
  }