@chalky/chalky 0.0.0-stage → 0.1.4

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 (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +45 -2
  3. package/app/SYSTEM.md +31 -0
  4. package/app/extensions/appdata/README.md +10 -0
  5. package/app/extensions/appdata/guide.md +4 -0
  6. package/app/extensions/appdata/index.ts +47 -0
  7. package/app/extensions/boards/browser.js +357 -0
  8. package/app/extensions/boards/guide.md +4 -0
  9. package/app/extensions/boards/index.ts +98 -0
  10. package/app/extensions/commands/browser.js +13 -0
  11. package/app/extensions/commands/guide.md +3 -0
  12. package/app/extensions/commands/index.ts +96 -0
  13. package/app/extensions/doodle/README.md +15 -0
  14. package/app/extensions/doodle/browser.js +398 -0
  15. package/app/extensions/doodle/guide.md +6 -0
  16. package/app/extensions/doodle/index.ts +54 -0
  17. package/app/extensions/files/browser.js +96 -0
  18. package/app/extensions/files/guide.md +7 -0
  19. package/app/extensions/files/index.ts +73 -0
  20. package/app/extensions/frames/README.md +37 -0
  21. package/app/extensions/frames/browser.js +166 -0
  22. package/app/extensions/frames/guide.md +7 -0
  23. package/app/extensions/frames/index.ts +85 -0
  24. package/app/extensions/login/browser.js +363 -0
  25. package/app/extensions/login/index.ts +214 -0
  26. package/app/extensions/mcp/browser.js +64 -0
  27. package/app/extensions/mcp/guide.md +5 -0
  28. package/app/extensions/mcp/index.ts +64 -0
  29. package/app/extensions/model/index.ts +20 -0
  30. package/app/extensions/page/index.ts +96 -0
  31. package/app/extensions/page/page.html +615 -0
  32. package/app/extensions/point/README.md +22 -0
  33. package/app/extensions/point/browser.js +247 -0
  34. package/app/extensions/point/guide.md +6 -0
  35. package/app/extensions/point/index.ts +112 -0
  36. package/app/extensions/prompt/index.ts +19 -0
  37. package/app/extensions/reload/index.ts +21 -0
  38. package/app/extensions/screen/browser.js +76 -0
  39. package/app/extensions/screen/index.ts +80 -0
  40. package/app/extensions/steps/index.ts +17 -0
  41. package/app/extensions/swarm/README.md +47 -0
  42. package/app/extensions/swarm/guide.md +11 -0
  43. package/app/extensions/swarm/index.ts +304 -0
  44. package/app/extensions/ui/README.md +69 -0
  45. package/app/extensions/ui/agents.js +45 -0
  46. package/app/extensions/ui/browser.js +364 -0
  47. package/app/extensions/ui/components/App.js +28 -0
  48. package/app/extensions/ui/components/Button.js +28 -0
  49. package/app/extensions/ui/components/Card.js +23 -0
  50. package/app/extensions/ui/components/Column.js +17 -0
  51. package/app/extensions/ui/components/Markdown.js +37 -0
  52. package/app/extensions/ui/components/Row.js +17 -0
  53. package/app/extensions/ui/components/Table.js +31 -0
  54. package/app/extensions/ui/components/Text.js +19 -0
  55. package/app/extensions/ui/components/TextField.js +30 -0
  56. package/app/extensions/ui/guide.md +7 -0
  57. package/app/extensions/ui/index.ts +291 -0
  58. package/app/extensions/ui/skills/agent-apps/SKILL.md +91 -0
  59. package/app/extensions/ui/skills/ui-check/SKILL.md +71 -0
  60. package/app/extensions/ui/skills/ui-check/scripts/check.mjs +306 -0
  61. package/app/extensions/ui/skills/ui-design/SKILL.md +123 -0
  62. package/app/extensions/ui/voice.js +114 -0
  63. package/app/extensions/ui_history/browser.js +97 -0
  64. package/app/extensions/ui_history/index.ts +109 -0
  65. package/app/extensions/versions/browser.js +58 -0
  66. package/app/extensions/versions/index.ts +168 -0
  67. package/app/extensions/viewer/README.md +12 -0
  68. package/app/extensions/viewer/browser.js +322 -0
  69. package/app/extensions/viewer/guide.md +5 -0
  70. package/app/extensions/viewer/index.ts +209 -0
  71. package/app/extensions/voice/browser.js +71 -0
  72. package/app/extensions/voice/index.ts +37 -0
  73. package/app/mcp.json +3 -0
  74. package/app/settings.json +3 -0
  75. package/app/skills/plugin-map/SKILL.md +49 -0
  76. package/app/skills/write-skill/SKILL.md +57 -0
  77. package/app/ui/apps/.keep +0 -0
  78. package/bin/chalky.mjs +11 -0
  79. package/node_modules/xlsx/CHANGELOG.md +361 -0
  80. package/node_modules/xlsx/LICENSE +201 -0
  81. package/node_modules/xlsx/README.md +41 -0
  82. package/node_modules/xlsx/bin/xlsx.njs +310 -0
  83. package/node_modules/xlsx/bower.json +22 -0
  84. package/node_modules/xlsx/dist/LICENSE +201 -0
  85. package/node_modules/xlsx/dist/cpexcel.d.ts +39 -0
  86. package/node_modules/xlsx/dist/cpexcel.full.mjs +1502 -0
  87. package/node_modules/xlsx/dist/cpexcel.js +1506 -0
  88. package/node_modules/xlsx/dist/shim.min.js +2 -0
  89. package/node_modules/xlsx/dist/xlsx.core.min.js +18 -0
  90. package/node_modules/xlsx/dist/xlsx.core.min.map +1 -0
  91. package/node_modules/xlsx/dist/xlsx.extendscript.js +28259 -0
  92. package/node_modules/xlsx/dist/xlsx.full.min.js +24 -0
  93. package/node_modules/xlsx/dist/xlsx.full.min.map +1 -0
  94. package/node_modules/xlsx/dist/xlsx.mini.min.js +10 -0
  95. package/node_modules/xlsx/dist/xlsx.mini.min.map +1 -0
  96. package/node_modules/xlsx/dist/xlsx.zahl.js +4 -0
  97. package/node_modules/xlsx/dist/xlsx.zahl.mjs +2 -0
  98. package/node_modules/xlsx/dist/zahl.d.ts +4 -0
  99. package/node_modules/xlsx/package.json +183 -0
  100. package/node_modules/xlsx/types/index.d.ts +1065 -0
  101. package/node_modules/xlsx/types/tsconfig.json +15 -0
  102. package/node_modules/xlsx/xlsx.js +28103 -0
  103. package/node_modules/xlsx/xlsx.mjs +28228 -0
  104. package/node_modules/xlsx/xlsxworker.js +14 -0
  105. package/package.json +42 -4
  106. package/server.ts +281 -0
@@ -0,0 +1,64 @@
1
+ // Browser half of the mcp extension: "/mcp" in the chat input opens a picker of the servers in app/mcp.json. A
2
+ // server's box turns it on or off; open it to tick single tools (greyed while the server is off). Each change is
3
+ // saved at once (mcp_set). A server's tools show once the agents have connected to it.
4
+ const STYLE = `
5
+ .mcp { width: min(560px, calc(100vw - 32px)); max-height: 75vh; padding: 16px 18px; box-sizing: border-box;
6
+ border-radius: var(--radius, 16px); border: 1px solid var(--edge, #eee); color: var(--text, #1d1d1f);
7
+ background: var(--glass-strong, #fff); backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
8
+ box-shadow: var(--shadow, 0 8px 30px rgba(0,0,0,.1)); font-size: 13px; }
9
+ .mcp::backdrop { background: rgba(0, 0, 0, .08); }
10
+ .mcp header { display: flex; justify-content: space-between; margin-bottom: 10px; font-weight: 600; }
11
+ .mcp header button { font: inherit; color: var(--muted, #888); background: none; border: 0; cursor: pointer; }
12
+ .mcp p { color: var(--muted, #888); margin: 0 0 10px; } .mcp p.error { color: #c62828; }
13
+ .mcp details { border-top: 1px solid rgba(128, 128, 128, .2); padding: 8px 0; }
14
+ .mcp summary { display: flex; gap: 8px; align-items: center; cursor: pointer; list-style: none; }
15
+ .mcp summary span { margin-left: auto; color: var(--muted, #888); }
16
+ .mcp label { display: flex; gap: 8px; align-items: baseline; padding: 4px 0 4px 24px; cursor: pointer; }
17
+ .mcp label span { color: var(--muted, #888); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
18
+ `;
19
+
20
+ export default function (page) {
21
+ const style = document.createElement("style");
22
+ style.textContent = STYLE;
23
+ document.head.append(style);
24
+ const box = document.createElement("dialog");
25
+ box.className = "mcp";
26
+ document.body.append(box);
27
+ box.onclick = (e) => { if (e.target === box) box.close(); }; // a click on the backdrop closes it
28
+ let state = null; // the "mcp" event: {file, servers, error}
29
+ const open = new Set(); // servers whose tools are shown, kept across redraws
30
+ const el = (tag, props, ...kids) => { const e = Object.assign(document.createElement(tag), props); e.append(...kids); return e; };
31
+ const tick = (checked, onchange, more = {}) => el("input", { type: "checkbox", checked, onchange, ...more });
32
+
33
+ function render() {
34
+ const close = el("button", { title: "Close", onclick: () => box.close() }, "✕");
35
+ box.replaceChildren(el("header", {}, "MCP tools", close));
36
+ if (!state) return box.append("Asking…");
37
+ box.append(el("p", {}, `Tick what the agents may use. The servers are in ${state.file}.`));
38
+ if (state.error) box.append(el("p", { className: "error" }, state.error));
39
+ const servers = Object.entries(state.servers);
40
+ if (!servers.length && !state.error) box.append(el("p", {}, "No servers yet: add them to that file."));
41
+ for (const [server, s] of servers) {
42
+ const on = s.tools.filter((t) => t.on).length;
43
+ const all = tick(s.enabled, () => page.send({ type: "mcp_set", server, on: all.checked }),
44
+ { onclick: (e) => e.stopPropagation() }); // ticking the server doesn't open or close it
45
+ const count = s.tools.length ? `${on}/${s.tools.length}` : s.enabled ? "no tools yet" : "off";
46
+ const d = el("details", { open: open.has(server) },
47
+ el("summary", {}, all, el("b", { title: s.description }, server), el("span", {}, count)));
48
+ d.ontoggle = () => d.open ? open.add(server) : open.delete(server);
49
+ for (const t of s.tools) {
50
+ const one = tick(t.on, () => page.send({ type: "mcp_set", server, tool: t.name, on: one.checked }), { disabled: !s.enabled });
51
+ d.append(el("label", { title: t.description }, one, t.name, el("span", {}, t.description)));
52
+ }
53
+ box.append(d);
54
+ }
55
+ }
56
+
57
+ page.command("mcp", () => {
58
+ state = null; // ask again each time: mcp.json or the connections may have changed
59
+ render();
60
+ box.showModal();
61
+ page.send({ type: "mcp_list" });
62
+ }, "Choose the MCP tools the agents may use");
63
+ page.on("mcp", (ev) => { state = ev; if (box.open) render(); });
64
+ }
@@ -0,0 +1,5 @@
1
+ # MCP servers
2
+ Servers are in {code}/mcp.json (pi's built-in MCP: pi's docs/mcp.md); their tools are named mcp__<server>__<tool>.
3
+ The user ticks which servers and tools are on with /mcp on the page.
4
+ - To add a server when asked: add its entry to that file (secrets as ${ENV_VAR} in headers or env, never written
5
+ out), then call reload.
@@ -0,0 +1,64 @@
1
+ // MCP picker: "/mcp" on the page ticks which servers in app/mcp.json, and which of their tools, the agents may use.
2
+ // pi's built-in MCP does the rest (server.ts loads it; pi's docs/mcp.md). A tick writes the fields pi's own /mcp
3
+ // screen writes, in that file: "enabled": false for a server, toolExposure "<tool>": "hidden" (or back to the
4
+ // server's exposure) for a tool. Then every agent reloads, one change at a time (a busy agent after its turn).
5
+ // Tools: agent1's, from pi (getAllTools), once its connection to the server is up.
6
+ // page message mcp_list -> event "mcp" {file, servers: {name: {enabled, description,
7
+ // mcp_set {server, on} | {server, tool, on} tools: [{name, description, on}]}}, error}
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
+
12
+ const host = (globalThis as any).chalky;
13
+ const FILE = path.join(host.app, "mcp.json");
14
+
15
+ function state() {
16
+ const servers = JSON.parse(fs.readFileSync(FILE, "utf8")).mcpServers ?? {};
17
+ const tools = host.agents.get("agent1")?.session.getAllTools() ?? [];
18
+ const out: Record<string, any> = {};
19
+ for (const [name, c] of Object.entries<any>(servers)) {
20
+ const enabled = c.enabled !== false;
21
+ out[name] = { enabled, description: c.description ?? c.url ?? c.command ?? "",
22
+ tools: tools.filter((t: any) => t.namespace?.aliases?.[0] === name).map((t: any) => {
23
+ const tool = t.name.slice(t.namespace.name.length + 2); // mcp__<server>__<tool>
24
+ return { name: tool, description: t.description, on: enabled && (c.toolExposure?.[tool] ?? t.exposure) !== "hidden" };
25
+ }) };
26
+ }
27
+ return { file: FILE, servers: out, error: "" };
28
+ }
29
+
30
+ function set(msg: any) {
31
+ const json = JSON.parse(fs.readFileSync(FILE, "utf8")), c = json.mcpServers?.[msg.server];
32
+ if (!c) return;
33
+ if (msg.tool === undefined) { if (msg.on) delete c.enabled; else c.enabled = false; }
34
+ else {
35
+ const own = c.exposure ?? "codemode"; // pi's default
36
+ (c.toolExposure ??= {})[msg.tool] = !msg.on ? "hidden" : own === "hidden" ? "direct" : own;
37
+ }
38
+ fs.writeFileSync(FILE, JSON.stringify(json, null, 2) + "\n");
39
+ }
40
+
41
+ async function reloadAll() {
42
+ for (const a of host.agents.values()) {
43
+ if (a.session.isStreaming) { if (!host.reloadAfterTurn.has(a.name)) host.reloadAfterTurn.set(a.name, true); }
44
+ else await host.reloadAgent(a.name, true); // quiet: ticking isn't news for the conversation
45
+ }
46
+ }
47
+
48
+ export default function (pi: ExtensionAPI) {
49
+ const guide = fs.readFileSync(path.join(import.meta.dirname, "guide.md"), "utf8");
50
+ pi.on("before_agent_start", (event) => { event.systemPromptOptions.sections.mcp = guide; });
51
+
52
+ host.pageHandlers.set("mcp", async (msg: any) => {
53
+ if (msg.type !== "mcp_list" && msg.type !== "mcp_set") return;
54
+ try {
55
+ if (msg.type === "mcp_set") set(msg);
56
+ host.broadcast("mcp", state()); // before the reloads: agent1's tools are still listed, the file has the ticks
57
+ } catch (e: any) {
58
+ return host.broadcast("mcp", { file: FILE, servers: {}, error: `${FILE}: ${e.message}` });
59
+ }
60
+ // reloads one after another, also across this extension's own reloads (host.shared outlives them)
61
+ if (msg.type === "mcp_set") host.shared.set("mcp-reloads", (host.shared.get("mcp-reloads") ?? Promise.resolve())
62
+ .then(reloadAll).catch((e: any) => console.error("mcp: reload failed:", e)));
63
+ });
64
+ }
@@ -0,0 +1,20 @@
1
+ // The model: one OpenAI-compatible endpoint (in Chalky: keyproxy -> LiteLLM), as the Python agent's llm plugin.
2
+ // MODEL_BASE_URL (up to /v1), MODEL_API_KEY, MODEL_NAME (a LiteLLM route, "default"). pi does the calls,
3
+ // streaming, retries, thinking and compaction; this only tells it where. The host selects provider "chalky".
4
+ // MODEL_CONTEXT_WINDOW: the model's context size; pi compacts when a request would leave less than 16384 tokens of it.
5
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
+
7
+ export default function (pi: ExtensionAPI) {
8
+ if (!process.env.MODEL_BASE_URL) return; // pi then says no model is configured on each message
9
+ pi.registerProvider("chalky", {
10
+ baseUrl: process.env.MODEL_BASE_URL,
11
+ apiKey: "$MODEL_API_KEY", // read from the environment by pi, not stored in the session
12
+ api: "openai-completions",
13
+ compat: { supportsStore: false, supportsDeveloperRole: false }, // LiteLLM may not take "store" (pi-ai docs)
14
+ models: [{
15
+ id: process.env.MODEL_NAME || "default", name: "Chalky default route", reasoning: true,
16
+ input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
17
+ contextWindow: Number(process.env.MODEL_CONTEXT_WINDOW || 200000), maxTokens: 16000,
18
+ }],
19
+ });
20
+ }
@@ -0,0 +1,96 @@
1
+ // The page the user sees (Python: chat_ui.py), as an extension like the others: agents can change it, and the
2
+ // versions extension keeps and rolls back its history.
3
+ // GET / page.html, read fresh on every load (edits show on reload)
4
+ // GET /page/scripts the browser parts the page imports: one per extension folder with a browser.js
5
+ // GET /extensions/<name>/browser.js that file (only browser.js: an extension's other files aren't served)
6
+ // An extension's browser.js is its part of the page: its default export gets the page's API (see page.html's header).
7
+ // Found by looking in each folder, as pi finds index.ts, so an extension has nothing to register.
8
+ // The conversation on the page: pi's messages in the page's format (OpenAI chat, as the Python harness stored them).
9
+ // event "message" {agent, role, content, ...} as an agent's pi session ends each message (host.eventHandlers)
10
+ // events "run_start", "run_end", "compacted" an agent's run started, ended (settled), its context was compacted
11
+ // on opening, a page gets every agent's record, in time order (host.connectHandlers): all its session files
12
+ // (compaction keeps the original entries, so the page still shows everything)
13
+ // "joined" (an agent's first system message) and reply_to (who a turn's answer goes to: the host's "chalky.turn"
14
+ // entry just before the user message that starts it) are read from the session, so nothing is kept here.
15
+ import fs from "node:fs";
16
+ import path from "node:path";
17
+ import { SessionManager, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
18
+
19
+ const host = (globalThis as any).chalky;
20
+ const EXTENSIONS = path.dirname(import.meta.dirname);
21
+ const browserFile = (name: string) => path.join(EXTENSIONS, name, "browser.js");
22
+
23
+ export default function (_pi: ExtensionAPI) {
24
+ host.routes.set("/", (_req: any, res: any) =>
25
+ res.writeHead(200, { "content-type": "text/html", "cache-control": "no-cache" }).end(fs.readFileSync(path.join(import.meta.dirname, "page.html"))));
26
+ host.routes.set("/page/scripts", (_req: any, res: any) => {
27
+ const names = fs.readdirSync(EXTENSIONS).filter((n) => fs.existsSync(browserFile(n))).sort();
28
+ res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(names.map((n) => `/extensions/${n}/browser.js`)));
29
+ });
30
+ host.routes.set("/extensions/", (req: any, res: any) => {
31
+ const [, , name, file] = new URL(req.url, "http://x").pathname.split("/");
32
+ if (file !== "browser.js" || !/^[\w-]+$/.test(name ?? "") || !fs.existsSync(browserFile(name))) return res.writeHead(404).end();
33
+ res.writeHead(200, { "content-type": "text/javascript", "cache-control": "no-cache" }).end(fs.readFileSync(browserFile(name)));
34
+ });
35
+ }
36
+
37
+ const text = (content: any) => typeof content === "string" ? content
38
+ : content.filter((p: any) => p.type === "text").map((p: any) => p.text).join("");
39
+
40
+ // pi message -> the page's message. first: the agent's first system message, shown as "<agent> joined".
41
+ // replyTo: on the message that starts a turn, who its answer goes to (not on items slipped into a running turn).
42
+ function toPage(m: any, first = false, replyTo?: string) {
43
+ const ts = (m.timestamp ?? Date.now()) / 1000;
44
+ if (m.role === "system") return first ? { role: "system", content: "", ts } : null;
45
+ if (m.role === "user" || (m.role === "custom" && m.customType === "inbox")) { // inbox: swarm items filed without a turn
46
+ return { role: "user", content: text(m.content), ...(replyTo && { reply_to: replyTo }), ts };
47
+ }
48
+ if (m.role === "assistant") {
49
+ const calls = m.content.filter((p: any) => p.type === "toolCall")
50
+ .map((p: any) => ({ id: p.id, type: "function", function: { name: p.name, arguments: JSON.stringify(p.arguments) } }));
51
+ const reasoning = m.content.filter((p: any) => p.type === "thinking").map((p: any) => p.thinking).join("\n");
52
+ return { role: "assistant", content: text(m.content), ...(reasoning && { reasoning }), ...(calls.length && { tool_calls: calls }),
53
+ ...(m.errorMessage && { content: `error: ${m.errorMessage}` }), ts };
54
+ }
55
+ if (m.role === "toolResult")
56
+ return { role: "tool", tool_call_id: m.toolCallId, content: (m.isError ? "error: " : "") + text(m.content), ts };
57
+ return null; // pi's own kinds (custom, compaction summary, ...): not shown
58
+ }
59
+
60
+ // Every message of an agent, from all its session files (older rounds first), in the page's format
61
+ function record(name: string) {
62
+ const sm = host.agents.get(name).session.sessionManager, current = sm.getSessionFile(), dir = path.dirname(current);
63
+ return fs.readdirSync(dir).filter((f) => f.endsWith(".jsonl")).sort().flatMap((f) => {
64
+ const p = path.join(dir, f);
65
+ const entries = p === current ? sm.getEntries() : SessionManager.open(p, dir).getEntries();
66
+ let joined = false, replyTo: string | undefined;
67
+ return entries.flatMap((e: any) => {
68
+ if (e.type === "custom" && e.customType === "chalky.turn") replyTo = e.data.reply_to;
69
+ const msg = e.type === "message" ? e.message : e.type === "custom_message" ? { ...e, role: "custom", timestamp: Date.parse(e.timestamp) } : null;
70
+ if (!msg) return [];
71
+ const m = toPage(msg, !joined && msg.role === "system", msg.role === "user" ? replyTo : undefined);
72
+ if (msg.role === "system") joined = true;
73
+ if (msg.role === "user") replyTo = undefined;
74
+ return m ? [{ agent: name, ...m }] : [];
75
+ });
76
+ });
77
+ }
78
+
79
+ // pi saves a message after its message_end, so the session holds what came before it
80
+ host.eventHandlers.set("page", (name: string, ev: any) => {
81
+ if (ev.type === "message_end") {
82
+ const entries = host.agents.get(name)?.session.sessionManager.getEntries() ?? [];
83
+ const first = ev.message.role === "system" && !entries.some((e: any) => e.type === "message" && e.message.role === "system");
84
+ const last = entries.findLast((e: any) => !(e.type === "message" && e.message.role === "system")); // pi may put a prompt update between
85
+ const replyTo = ev.message.role === "user" && last?.type === "custom" && last.customType === "chalky.turn" ? last.data.reply_to : undefined;
86
+ const m = toPage(ev.message, first, replyTo);
87
+ if (m) host.broadcast("message", { agent: name, ...m });
88
+ } else if (ev.type === "agent_start") host.broadcast("run_start", { agent: name });
89
+ else if (ev.type === "agent_settled") host.broadcast("run_end", { agent: name });
90
+ else if (ev.type === "compaction_end" && ev.result)
91
+ host.broadcast("compacted", { agent: name, tokens_before: ev.result.tokensBefore, tokens_after: ev.result.estimatedTokensAfter ?? "?" });
92
+ });
93
+
94
+ host.connectHandlers.set("page", (send: (type: string, data: object) => void) => {
95
+ for (const m of [...host.agents.keys()].flatMap(record).sort((x: any, y: any) => x.ts - y.ts)) send("message", m);
96
+ });