@ch4p/cli 0.1.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.
@@ -0,0 +1,12 @@
1
+ import {
2
+ audit,
3
+ performAudit,
4
+ runAudit
5
+ } from "./chunk-TEVLTQYT.js";
6
+ import "./chunk-NRFRTZVP.js";
7
+ import "./chunk-NMGPBPNU.js";
8
+ export {
9
+ audit,
10
+ performAudit,
11
+ runAudit
12
+ };
@@ -0,0 +1,313 @@
1
+ import {
2
+ CanvasSessionManager,
3
+ CanvasTool,
4
+ GatewayServer,
5
+ PairingManager,
6
+ SessionManager
7
+ } from "./chunk-GEFQONOB.js";
8
+ import {
9
+ DefaultSecurityPolicy,
10
+ NativeEngine,
11
+ ProviderRegistry,
12
+ createClaudeCliEngine,
13
+ createCodexCliEngine,
14
+ createMemoryBackend,
15
+ createObserver
16
+ } from "./chunk-7EUURDQ5.js";
17
+ import {
18
+ LoadSkillTool,
19
+ ToolRegistry
20
+ } from "./chunk-PGZ24EFT.js";
21
+ import {
22
+ SkillRegistry
23
+ } from "./chunk-6BURGD2Y.js";
24
+ import {
25
+ AgentLoop
26
+ } from "./chunk-IRNN57EQ.js";
27
+ import {
28
+ generateId
29
+ } from "./chunk-YSCX2QQQ.js";
30
+ import {
31
+ getLogsDir,
32
+ loadConfig
33
+ } from "./chunk-NRFRTZVP.js";
34
+ import {
35
+ BOLD,
36
+ DIM,
37
+ GREEN,
38
+ RED,
39
+ RESET,
40
+ TEAL,
41
+ separator
42
+ } from "./chunk-NMGPBPNU.js";
43
+
44
+ // src/commands/canvas.ts
45
+ import { resolve, dirname } from "path";
46
+ import { fileURLToPath } from "url";
47
+ import { execSync } from "child_process";
48
+ async function canvas(args) {
49
+ let config;
50
+ try {
51
+ config = loadConfig();
52
+ } catch (err) {
53
+ const message = err instanceof Error ? err.message : String(err);
54
+ console.error(`
55
+ ${RED}Failed to load config:${RESET} ${message}`);
56
+ console.error(` ${DIM}Run ${TEAL}ch4p onboard${DIM} to set up ch4p.${RESET}
57
+ `);
58
+ process.exitCode = 1;
59
+ return;
60
+ }
61
+ let port = config.canvas?.port ?? config.gateway.port ?? 4800;
62
+ let autoOpen = true;
63
+ for (let i = 0; i < args.length; i++) {
64
+ if (args[i] === "--port" && args[i + 1]) {
65
+ const parsed = parseInt(args[i + 1], 10);
66
+ if (!isNaN(parsed) && parsed > 0 && parsed <= 65535) {
67
+ port = parsed;
68
+ }
69
+ }
70
+ if (args[i] === "--no-open") {
71
+ autoOpen = false;
72
+ }
73
+ }
74
+ const host = "127.0.0.1";
75
+ const __dirname = dirname(fileURLToPath(import.meta.url));
76
+ const staticDir = resolve(__dirname, "..", "..", "..", "apps", "web", "dist");
77
+ const sessionManager = new SessionManager();
78
+ const pairingManager = config.canvas?.requirePairing ? new PairingManager() : void 0;
79
+ const canvasSessionManager = new CanvasSessionManager();
80
+ const engine = createCanvasEngine(config);
81
+ if (!engine) {
82
+ console.error(`
83
+ ${RED}No LLM engine available.${RESET}`);
84
+ console.error(` ${DIM}Ensure an API key is configured. Run ${TEAL}ch4p onboard${DIM}.${RESET}
85
+ `);
86
+ process.exitCode = 1;
87
+ return;
88
+ }
89
+ const obsCfg = {
90
+ observers: config.observability.observers ?? ["console"],
91
+ logLevel: config.observability.logLevel ?? "info",
92
+ logPath: `${getLogsDir()}/canvas.jsonl`
93
+ };
94
+ const observer = createObserver(obsCfg);
95
+ let memoryBackend;
96
+ try {
97
+ const memCfg = {
98
+ backend: config.memory.backend,
99
+ vectorWeight: config.memory.vectorWeight,
100
+ keywordWeight: config.memory.keywordWeight,
101
+ embeddingProvider: config.memory.embeddingProvider,
102
+ openaiApiKey: config.providers?.openai?.apiKey || void 0
103
+ };
104
+ memoryBackend = createMemoryBackend(memCfg);
105
+ } catch {
106
+ }
107
+ let skillRegistry;
108
+ try {
109
+ if (config.skills?.enabled && config.skills?.paths?.length) {
110
+ skillRegistry = SkillRegistry.createFromPaths(config.skills.paths);
111
+ }
112
+ } catch {
113
+ }
114
+ const sessionId = generateId(16);
115
+ const defaultSessionConfig = {
116
+ engineId: config.engines?.default ?? "native",
117
+ model: config.agent.model,
118
+ provider: config.agent.provider,
119
+ systemPrompt: "You are ch4p, an AI assistant with an interactive canvas workspace. You can render visual components on the canvas using the canvas_render tool. Available component types: card, chart, form, button, text_field, data_table, code_block, markdown, image, progress, status. Components are placed at (x, y) positions on a spatial canvas. You can connect components with directional edges to show relationships. Use the canvas to create rich, visual responses when appropriate."
120
+ };
121
+ const server = new GatewayServer({
122
+ port,
123
+ host,
124
+ sessionManager,
125
+ pairingManager,
126
+ canvasSessionManager,
127
+ staticDir,
128
+ defaultSessionConfig,
129
+ onCanvasConnection: (connSessionId, bridge) => {
130
+ wireCanvasSession(
131
+ connSessionId,
132
+ bridge,
133
+ canvasSessionManager,
134
+ engine,
135
+ config,
136
+ observer,
137
+ memoryBackend,
138
+ skillRegistry,
139
+ defaultSessionConfig
140
+ );
141
+ }
142
+ });
143
+ console.log(`
144
+ ${TEAL}${BOLD}ch4p Canvas${RESET}`);
145
+ console.log(separator());
146
+ console.log("");
147
+ try {
148
+ await server.start();
149
+ } catch (err) {
150
+ const message = err instanceof Error ? err.message : String(err);
151
+ console.error(` ${RED}Failed to start server:${RESET} ${message}`);
152
+ process.exitCode = 1;
153
+ return;
154
+ }
155
+ const addr = server.getAddress();
156
+ const bindDisplay = addr ? `${addr.host}:${addr.port}` : `${host}:${port}`;
157
+ const url = `http://${bindDisplay}/?session=${sessionId}`;
158
+ console.log(` ${GREEN}${BOLD}Server listening${RESET} on ${bindDisplay}`);
159
+ console.log(` ${BOLD}Session${RESET} ${sessionId}`);
160
+ console.log(` ${BOLD}Engine${RESET} ${engine.name}`);
161
+ console.log(` ${BOLD}Static dir${RESET} ${DIM}${staticDir}${RESET}`);
162
+ console.log("");
163
+ console.log(` ${BOLD}Routes:${RESET}`);
164
+ console.log(` ${DIM} WS /ws/:sessionId - WebSocket canvas connection${RESET}`);
165
+ console.log(` ${DIM} GET /health - liveness probe${RESET}`);
166
+ console.log(` ${DIM} GET /* - static files (web UI)${RESET}`);
167
+ console.log("");
168
+ if (pairingManager) {
169
+ const code = pairingManager.generateCode("Canvas startup");
170
+ console.log(` ${BOLD}Pairing code:${RESET} ${TEAL}${BOLD}${code.code}${RESET}`);
171
+ console.log(` ${DIM}Add ?token=YOUR_TOKEN to the URL after pairing.${RESET}`);
172
+ console.log("");
173
+ }
174
+ console.log(` ${GREEN}${BOLD}Canvas URL:${RESET} ${TEAL}${url}${RESET}`);
175
+ console.log("");
176
+ if (autoOpen) {
177
+ try {
178
+ const platform = process.platform;
179
+ const openCmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
180
+ execSync(`${openCmd} "${url}"`, { stdio: "ignore" });
181
+ console.log(` ${DIM}Browser opened.${RESET}`);
182
+ } catch {
183
+ console.log(` ${DIM}Couldn't auto-open browser. Open the URL above manually.${RESET}`);
184
+ }
185
+ }
186
+ console.log(` ${DIM}Press Ctrl+C to stop.${RESET}
187
+ `);
188
+ await new Promise((resolvePromise) => {
189
+ const shutdown = async () => {
190
+ console.log(`
191
+ ${DIM}Shutting down canvas...${RESET}`);
192
+ canvasSessionManager.endAll();
193
+ if (memoryBackend) {
194
+ try {
195
+ await memoryBackend.close();
196
+ } catch {
197
+ }
198
+ }
199
+ await server.stop();
200
+ await observer.flush?.();
201
+ console.log(` ${DIM}Goodbye!${RESET}
202
+ `);
203
+ resolvePromise();
204
+ };
205
+ process.on("SIGINT", () => void shutdown());
206
+ process.on("SIGTERM", () => void shutdown());
207
+ });
208
+ }
209
+ function createCanvasEngine(config) {
210
+ const engineId = config.engines?.default ?? "native";
211
+ const engineConfig = config.engines?.available?.[engineId];
212
+ if (engineId === "claude-cli") {
213
+ try {
214
+ return createClaudeCliEngine({
215
+ command: engineConfig?.command ?? void 0,
216
+ cwd: engineConfig?.cwd ?? void 0,
217
+ timeout: engineConfig?.timeout ?? void 0
218
+ });
219
+ } catch {
220
+ }
221
+ }
222
+ if (engineId === "codex-cli") {
223
+ try {
224
+ return createCodexCliEngine({
225
+ command: engineConfig?.command ?? void 0,
226
+ cwd: engineConfig?.cwd ?? void 0,
227
+ timeout: engineConfig?.timeout ?? void 0
228
+ });
229
+ } catch {
230
+ }
231
+ }
232
+ const providerName = config.agent.provider;
233
+ const providerConfig = config.providers?.[providerName];
234
+ const apiKey = providerConfig?.apiKey;
235
+ if (providerName !== "ollama" && (!apiKey || apiKey.trim().length === 0)) {
236
+ return null;
237
+ }
238
+ try {
239
+ const provider = ProviderRegistry.createProvider({
240
+ id: providerName,
241
+ type: providerName,
242
+ ...providerConfig
243
+ });
244
+ return new NativeEngine({ provider, defaultModel: config.agent.model });
245
+ } catch {
246
+ return null;
247
+ }
248
+ }
249
+ function wireCanvasSession(sessionId, bridge, canvasSessionManager, engine, config, observer, memoryBackend, skillRegistry, defaultSessionConfig) {
250
+ const entry = canvasSessionManager.getSession(sessionId);
251
+ if (!entry) return;
252
+ const { canvasState, canvasChannel } = entry;
253
+ canvasChannel.start({ sessionId }).catch(() => {
254
+ });
255
+ canvasChannel.onMessage((msg) => {
256
+ void (async () => {
257
+ try {
258
+ if (msg.text.startsWith("[ABORT]")) {
259
+ return;
260
+ }
261
+ const session = new (await import("./dist-VZE4JK7Q.js")).Session({
262
+ sessionId,
263
+ ...defaultSessionConfig
264
+ });
265
+ const exclude = config.autonomy.level === "readonly" ? ["bash", "file_write", "file_edit", "delegate"] : ["delegate"];
266
+ if (!config.mesh?.enabled) {
267
+ exclude.push("mesh");
268
+ }
269
+ const tools = ToolRegistry.createDefault({ exclude });
270
+ tools.register(new CanvasTool());
271
+ if (skillRegistry && skillRegistry.size > 0) {
272
+ tools.register(new LoadSkillTool(skillRegistry));
273
+ }
274
+ const securityPolicy = new DefaultSecurityPolicy({
275
+ workspace: process.cwd(),
276
+ autonomyLevel: config.autonomy.level,
277
+ allowedCommands: config.autonomy.allowedCommands,
278
+ blockedPaths: config.security.blockedPaths
279
+ });
280
+ const toolContextExtensions = {
281
+ canvasState
282
+ };
283
+ if (config.search?.enabled && config.search.apiKey) {
284
+ toolContextExtensions.searchApiKey = config.search.apiKey;
285
+ toolContextExtensions.searchConfig = {
286
+ maxResults: config.search.maxResults,
287
+ country: config.search.country,
288
+ searchLang: config.search.searchLang
289
+ };
290
+ }
291
+ const loop = new AgentLoop(session, engine, tools.list(), observer, {
292
+ maxIterations: 30,
293
+ maxRetries: 2,
294
+ enableStateSnapshots: true,
295
+ memoryBackend,
296
+ securityPolicy,
297
+ toolContextExtensions
298
+ });
299
+ for await (const event of loop.run(msg.text)) {
300
+ bridge.handleAgentEvent(event);
301
+ }
302
+ } catch (err) {
303
+ bridge.handleAgentEvent({
304
+ type: "error",
305
+ error: err instanceof Error ? err : new Error(String(err))
306
+ });
307
+ }
308
+ })();
309
+ });
310
+ }
311
+ export {
312
+ canvas
313
+ };
@@ -0,0 +1,290 @@
1
+ // ../../packages/skills/dist/index.js
2
+ import { readdirSync, readFileSync, existsSync, statSync } from "fs";
3
+ import { resolve, basename, join } from "path";
4
+ import { homedir } from "os";
5
+ var SkillParseError = class extends Error {
6
+ constructor(message, path) {
7
+ super(message);
8
+ this.path = path;
9
+ this.name = "SkillParseError";
10
+ }
11
+ };
12
+ var NAME_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
13
+ var MAX_NAME_LENGTH = 64;
14
+ var MAX_DESCRIPTION_LENGTH = 1024;
15
+ function parseSkillManifest(content, filePath) {
16
+ const trimmed = content.trimStart();
17
+ if (!trimmed.startsWith("---")) {
18
+ throw new SkillParseError(
19
+ "SKILL.md must start with YAML frontmatter (---)",
20
+ filePath
21
+ );
22
+ }
23
+ const afterOpening = trimmed.slice(3);
24
+ const closingIndex = afterOpening.indexOf("\n---");
25
+ if (closingIndex === -1) {
26
+ throw new SkillParseError(
27
+ "SKILL.md frontmatter is missing closing ---",
28
+ filePath
29
+ );
30
+ }
31
+ const yamlBlock = afterOpening.slice(0, closingIndex).trim();
32
+ const body = afterOpening.slice(closingIndex + 4).trim();
33
+ const manifest = parseYamlBlock(yamlBlock, filePath);
34
+ validateManifest(manifest, filePath);
35
+ return { manifest, body };
36
+ }
37
+ function parseYamlBlock(yaml, _filePath) {
38
+ const lines = yaml.split("\n");
39
+ const result = {};
40
+ let currentKey = null;
41
+ let multilineValue = "";
42
+ let inMultiline = false;
43
+ let inNestedObject = false;
44
+ let nestedKey = null;
45
+ let nestedObj = {};
46
+ for (let i = 0; i < lines.length; i++) {
47
+ const line = lines[i];
48
+ if (inMultiline) {
49
+ if (line.match(/^\s{2,}/) || line.trim() === "") {
50
+ multilineValue += (multilineValue ? "\n" : "") + line.replace(/^\s{2,}/, "");
51
+ continue;
52
+ } else {
53
+ result[currentKey] = multilineValue.trim();
54
+ inMultiline = false;
55
+ currentKey = null;
56
+ multilineValue = "";
57
+ }
58
+ }
59
+ if (inNestedObject) {
60
+ const nestedMatch = line.match(/^\s{2,}(\w[\w-]*):\s*(.+)/);
61
+ if (nestedMatch) {
62
+ nestedObj[nestedMatch[1]] = stripQuotes(nestedMatch[2].trim());
63
+ continue;
64
+ } else {
65
+ result[nestedKey] = Object.keys(nestedObj).length > 0 ? { ...nestedObj } : "";
66
+ inNestedObject = false;
67
+ nestedKey = null;
68
+ nestedObj = {};
69
+ }
70
+ }
71
+ if (line.trim() === "") continue;
72
+ const kvMatch = line.match(/^(\w[\w-]*):\s*(.*)/);
73
+ if (!kvMatch) continue;
74
+ const key = kvMatch[1];
75
+ const rawValue = kvMatch[2].trim();
76
+ if (rawValue === "|") {
77
+ currentKey = key;
78
+ inMultiline = true;
79
+ multilineValue = "";
80
+ } else if (rawValue === "") {
81
+ nestedKey = key;
82
+ inNestedObject = true;
83
+ nestedObj = {};
84
+ } else if (rawValue.startsWith("[")) {
85
+ result[key] = parseInlineArray(rawValue);
86
+ } else {
87
+ result[key] = stripQuotes(rawValue);
88
+ }
89
+ }
90
+ if (inMultiline && currentKey) {
91
+ result[currentKey] = multilineValue.trim();
92
+ }
93
+ if (inNestedObject && nestedKey) {
94
+ result[nestedKey] = Object.keys(nestedObj).length > 0 ? { ...nestedObj } : "";
95
+ }
96
+ return {
97
+ name: String(result.name ?? ""),
98
+ description: String(result.description ?? ""),
99
+ license: result.license ? String(result.license) : void 0,
100
+ compatibility: Array.isArray(result.compatibility) ? result.compatibility.map(String) : void 0,
101
+ metadata: result.metadata && typeof result.metadata === "object" ? result.metadata : void 0
102
+ };
103
+ }
104
+ function parseInlineArray(raw) {
105
+ const inner = raw.replace(/^\[/, "").replace(/\]$/, "").trim();
106
+ if (!inner) return [];
107
+ return inner.split(",").map((item) => stripQuotes(item.trim()));
108
+ }
109
+ function stripQuotes(value) {
110
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
111
+ return value.slice(1, -1);
112
+ }
113
+ return value;
114
+ }
115
+ function validateManifest(manifest, filePath) {
116
+ if (!manifest.name) {
117
+ throw new SkillParseError("Skill manifest is missing required field: name", filePath);
118
+ }
119
+ if (!NAME_REGEX.test(manifest.name)) {
120
+ throw new SkillParseError(
121
+ `Invalid skill name "${manifest.name}": must be lowercase alphanumeric with hyphens (a-z, 0-9, -)`,
122
+ filePath
123
+ );
124
+ }
125
+ if (manifest.name.length > MAX_NAME_LENGTH) {
126
+ throw new SkillParseError(
127
+ `Skill name "${manifest.name}" exceeds maximum length of ${MAX_NAME_LENGTH} characters`,
128
+ filePath
129
+ );
130
+ }
131
+ if (!manifest.description) {
132
+ throw new SkillParseError(
133
+ "Skill manifest is missing required field: description",
134
+ filePath
135
+ );
136
+ }
137
+ if (manifest.description.length > MAX_DESCRIPTION_LENGTH) {
138
+ throw new SkillParseError(
139
+ `Skill description exceeds maximum length of ${MAX_DESCRIPTION_LENGTH} characters`,
140
+ filePath
141
+ );
142
+ }
143
+ }
144
+ function classifySource(dirPath) {
145
+ const home = homedir();
146
+ if (dirPath.startsWith(resolve(home, ".ch4p"))) return "global";
147
+ if (dirPath.includes(".agents/skills")) return "legacy";
148
+ return "project";
149
+ }
150
+ function expandTilde(p) {
151
+ if (p.startsWith("~/") || p === "~") {
152
+ return resolve(homedir(), p.slice(2));
153
+ }
154
+ return p;
155
+ }
156
+ function loadSkill(skillDir) {
157
+ const skillPath = join(skillDir, "SKILL.md");
158
+ if (!existsSync(skillPath)) return null;
159
+ const content = readFileSync(skillPath, "utf8");
160
+ const { manifest, body } = parseSkillManifest(content, skillPath);
161
+ const dirName = basename(skillDir);
162
+ if (dirName.toLowerCase() !== manifest.name.toLowerCase()) {
163
+ throw new SkillParseError(
164
+ `Directory name "${dirName}" does not match manifest name "${manifest.name}"`,
165
+ skillPath
166
+ );
167
+ }
168
+ const source = classifySource(skillDir);
169
+ return { manifest, body, path: skillPath, source };
170
+ }
171
+ function discoverSkills(searchPaths) {
172
+ const skillMap = /* @__PURE__ */ new Map();
173
+ for (const rawPath of searchPaths) {
174
+ const searchDir = expandTilde(rawPath);
175
+ if (!existsSync(searchDir)) continue;
176
+ let entries;
177
+ try {
178
+ entries = readdirSync(searchDir);
179
+ } catch {
180
+ continue;
181
+ }
182
+ for (const entry of entries) {
183
+ const fullPath = resolve(searchDir, entry);
184
+ try {
185
+ if (!statSync(fullPath).isDirectory()) continue;
186
+ } catch {
187
+ continue;
188
+ }
189
+ try {
190
+ const skill = loadSkill(fullPath);
191
+ if (skill) {
192
+ skillMap.set(skill.manifest.name, skill);
193
+ }
194
+ } catch (err) {
195
+ if (err instanceof SkillParseError) {
196
+ } else {
197
+ throw err;
198
+ }
199
+ }
200
+ }
201
+ }
202
+ return Array.from(skillMap.values());
203
+ }
204
+ var SkillRegistry = class _SkillRegistry {
205
+ skills = /* @__PURE__ */ new Map();
206
+ /**
207
+ * Register a skill. Throws if a skill with the same name is already registered.
208
+ */
209
+ register(skill) {
210
+ if (this.skills.has(skill.manifest.name)) {
211
+ throw new SkillParseError(
212
+ `Skill "${skill.manifest.name}" is already registered.`,
213
+ skill.path
214
+ );
215
+ }
216
+ this.skills.set(skill.manifest.name, skill);
217
+ }
218
+ /**
219
+ * Get a skill by name. Returns undefined if not found.
220
+ */
221
+ get(name) {
222
+ return this.skills.get(name);
223
+ }
224
+ /**
225
+ * Check whether a skill is registered.
226
+ */
227
+ has(name) {
228
+ return this.skills.has(name);
229
+ }
230
+ /**
231
+ * List all registered skills.
232
+ */
233
+ list() {
234
+ return Array.from(this.skills.values());
235
+ }
236
+ /**
237
+ * List skill names.
238
+ */
239
+ names() {
240
+ return Array.from(this.skills.keys());
241
+ }
242
+ /**
243
+ * Unregister a skill by name. Returns true if removed, false if not found.
244
+ */
245
+ unregister(name) {
246
+ return this.skills.delete(name);
247
+ }
248
+ /**
249
+ * Get skill descriptions for system prompt injection.
250
+ * Returns name + description pairs for progressive disclosure.
251
+ */
252
+ getDescriptions() {
253
+ return this.list().map((skill) => ({
254
+ name: skill.manifest.name,
255
+ description: skill.manifest.description
256
+ }));
257
+ }
258
+ /**
259
+ * Get the full markdown body for a skill (on-demand loading).
260
+ * Returns the instruction content that gets injected into context
261
+ * when the agent determines a skill is relevant.
262
+ */
263
+ getSkillContext(name) {
264
+ return this.skills.get(name)?.body;
265
+ }
266
+ /**
267
+ * Get the count of registered skills.
268
+ */
269
+ get size() {
270
+ return this.skills.size;
271
+ }
272
+ /**
273
+ * Create a registry by discovering skills from search paths.
274
+ *
275
+ * Scans directories for SKILL.md files, parses manifests, and registers
276
+ * all valid skills. Invalid manifests are silently skipped.
277
+ */
278
+ static createFromPaths(searchPaths) {
279
+ const registry = new _SkillRegistry();
280
+ const skills = discoverSkills(searchPaths);
281
+ for (const skill of skills) {
282
+ registry.register(skill);
283
+ }
284
+ return registry;
285
+ }
286
+ };
287
+
288
+ export {
289
+ SkillRegistry
290
+ };