clankie 0.2.2 → 0.2.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 (40) hide show
  1. package/README.md +17 -16
  2. package/dist/cli.js +301862 -0
  3. package/dist/koffi-216xhpes.node +0 -0
  4. package/dist/koffi-2erktc37.node +0 -0
  5. package/dist/koffi-2rrez93a.node +0 -0
  6. package/dist/koffi-2wv0r22g.node +0 -0
  7. package/dist/koffi-3kae4xj3.node +0 -0
  8. package/dist/koffi-3rkr2zqv.node +0 -0
  9. package/dist/koffi-abxfktv9.node +0 -0
  10. package/dist/koffi-c67c0c5b.node +0 -0
  11. package/dist/koffi-cnf0q0dx.node +0 -0
  12. package/dist/koffi-df38sqz5.node +0 -0
  13. package/dist/koffi-gfbqb3a0.node +0 -0
  14. package/dist/koffi-kjemmmem.node +0 -0
  15. package/dist/koffi-kkrfq9yv.node +0 -0
  16. package/dist/koffi-mzaqwwqy.node +0 -0
  17. package/dist/koffi-q49fgkeq.node +0 -0
  18. package/dist/koffi-q54bk8bf.node +0 -0
  19. package/dist/koffi-x1790w0j.node +0 -0
  20. package/dist/koffi-yxvjwcj6.node +0 -0
  21. package/package.json +8 -7
  22. package/web-ui-dist/_shell.html +2 -2
  23. package/web-ui-dist/assets/{card-BUP-xovx.js → card-Ce8RCN8-.js} +1 -1
  24. package/web-ui-dist/assets/{extensions-DC620Nmx.js → extensions-D-3Wl_TA.js} +1 -1
  25. package/web-ui-dist/assets/{index-DurjG9O_.js → index-ClDMn-6f.js} +1 -1
  26. package/web-ui-dist/assets/{loader-circle-DbOtKfCA.js → loader-circle-CpT1_nns.js} +1 -1
  27. package/web-ui-dist/assets/{main-B2sRcuyZ.js → main-J9rrgTOF.js} +4 -4
  28. package/web-ui-dist/assets/{sessions._sessionId-BJazw9EJ.js → sessions._sessionId-D6gfJDaW.js} +2 -2
  29. package/web-ui-dist/assets/{settings-Bv8oeIho.js → settings-ZDTymG3K.js} +1 -1
  30. package/web-ui-dist/manifest.json +23 -23
  31. package/src/agent.ts +0 -118
  32. package/src/channels/channel.ts +0 -57
  33. package/src/channels/slack.ts +0 -376
  34. package/src/channels/web.ts +0 -1375
  35. package/src/cli.ts +0 -505
  36. package/src/config.ts +0 -261
  37. package/src/daemon.ts +0 -380
  38. package/src/extensions/workspace-jail.ts +0 -171
  39. package/src/service.ts +0 -374
  40. package/src/sessions.ts +0 -262
@@ -1,171 +0,0 @@
1
- /**
2
- * Workspace Jail Extension
3
- *
4
- * Restricts agent file/command access to the workspace directory.
5
- * Prevents the agent from reading, writing, or executing commands
6
- * that reference paths outside the configured workspace.
7
- *
8
- * Uses pi's tool_call event to intercept and validate all tool calls.
9
- */
10
-
11
- import { existsSync, realpathSync } from "node:fs";
12
- import { isAbsolute, normalize, resolve } from "node:path";
13
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
14
-
15
- /**
16
- * Create a workspace jail extension that restricts file access.
17
- *
18
- * @param workspaceDir - Absolute path to the workspace directory
19
- * @param allowedPaths - Additional paths outside workspace that should be permitted
20
- */
21
- export function createWorkspaceJailExtension(workspaceDir: string, allowedPaths: string[] = []) {
22
- // Normalize workspace path and ensure it ends with /
23
- const normalizedWorkspace = `${normalize(workspaceDir).replace(/\/$/, "")}/`;
24
-
25
- // Normalize allowed paths (resolve to absolute, real paths where they exist)
26
- const normalizedAllowedPaths = allowedPaths.map((p) => {
27
- const resolved = resolve(p);
28
- try {
29
- return existsSync(resolved) ? realpathSync(resolved) : resolved;
30
- } catch {
31
- return resolved;
32
- }
33
- });
34
-
35
- return function workspaceJail(pi: ExtensionAPI) {
36
- /**
37
- * Check if a path is within the workspace or allowed paths.
38
- * Handles symlinks, relative paths, ~ expansion, etc.
39
- */
40
- function isPathAllowed(inputPath: string): { allowed: boolean; reason?: string } {
41
- // Resolve to absolute path
42
- let absolutePath: string;
43
-
44
- // Handle ~ expansion
45
- if (inputPath.startsWith("~/")) {
46
- return { allowed: false, reason: "Access to home directory (~/) is blocked" };
47
- }
48
- if (inputPath === "~") {
49
- return { allowed: false, reason: "Access to home directory (~) is blocked" };
50
- }
51
-
52
- // Resolve relative/absolute paths against workspace
53
- absolutePath = isAbsolute(inputPath) ? inputPath : resolve(workspaceDir, inputPath);
54
-
55
- // Resolve symlinks if the path exists
56
- let realPath: string;
57
- try {
58
- realPath = existsSync(absolutePath) ? realpathSync(absolutePath) : absolutePath;
59
- } catch {
60
- // If realpathSync fails (permission denied, etc), use the absolute path
61
- realPath = absolutePath;
62
- }
63
-
64
- // Normalize for comparison
65
- const normalized = `${normalize(realPath).replace(/\/$/, "")}/`;
66
-
67
- // Check if within workspace
68
- if (normalized.startsWith(normalizedWorkspace)) {
69
- return { allowed: true };
70
- }
71
-
72
- // Check if within allowed paths
73
- for (const allowedPath of normalizedAllowedPaths) {
74
- const normalizedAllowed = `${normalize(allowedPath).replace(/\/$/, "")}/`;
75
- if (normalized.startsWith(normalizedAllowed)) {
76
- return { allowed: true };
77
- }
78
- }
79
-
80
- return {
81
- allowed: false,
82
- reason: `Access denied: path '${inputPath}' is outside workspace (${workspaceDir})`,
83
- };
84
- }
85
-
86
- /**
87
- * Scan a bash command for obvious path escapes.
88
- * This is defense-in-depth, not a complete sandbox.
89
- */
90
- function scanBashCommand(command: string): { allowed: boolean; reason?: string } {
91
- // Block absolute paths outside workspace
92
- const absolutePathPattern = /(?:^|\s)([~/][\w\-./]+)/g;
93
- let match: RegExpExecArray | null;
94
-
95
- // biome-ignore lint/suspicious/noAssignInExpressions: regex exec pattern
96
- while ((match = absolutePathPattern.exec(command)) !== null) {
97
- const pathLike = match[1];
98
-
99
- // Check if it looks like a path to a sensitive location
100
- if (
101
- pathLike.startsWith("/etc") ||
102
- pathLike.startsWith("/var") ||
103
- pathLike.startsWith("/usr") ||
104
- pathLike.startsWith("/sys") ||
105
- pathLike.startsWith("/proc") ||
106
- pathLike.startsWith("/root") ||
107
- pathLike.startsWith("~/")
108
- ) {
109
- return {
110
- allowed: false,
111
- reason: `Blocked: command references path outside workspace: ${pathLike}`,
112
- };
113
- }
114
- }
115
-
116
- // Block cd to absolute paths outside workspace
117
- if (/\bcd\s+\//.test(command)) {
118
- return { allowed: false, reason: "Blocked: 'cd /' or 'cd /path' outside workspace" };
119
- }
120
-
121
- // Block obvious .. traversal attempts that escape workspace
122
- // This is heuristic - catches patterns like "cd ../../.." but won't catch all evasion
123
- const dotsPattern = /(?:^|\s)cd\s+(?:\.\.\/){3,}/;
124
- if (dotsPattern.test(command)) {
125
- return {
126
- allowed: false,
127
- reason: "Blocked: command attempts to traverse outside workspace using '..'",
128
- };
129
- }
130
-
131
- return { allowed: true };
132
- }
133
-
134
- // Intercept all tool calls
135
- pi.on("tool_call", async (event) => {
136
- const { toolName, input } = event;
137
-
138
- // File tools: validate the 'path' parameter
139
- if (["read", "write", "edit", "grep", "find", "ls"].includes(toolName)) {
140
- const toolInput = input as { path?: string };
141
- if (toolInput.path) {
142
- const check = isPathAllowed(toolInput.path);
143
- if (!check.allowed) {
144
- return { block: true, reason: check.reason };
145
- }
146
- }
147
- }
148
-
149
- // Bash: scan command for obvious path escapes
150
- if (toolName === "bash") {
151
- const toolInput = input as { command?: string };
152
- if (toolInput.command) {
153
- const check = scanBashCommand(toolInput.command);
154
- if (!check.allowed) {
155
- return { block: true, reason: check.reason };
156
- }
157
- }
158
- }
159
-
160
- return undefined;
161
- });
162
-
163
- // Inject system prompt reminder
164
- pi.on("before_agent_start", async () => {
165
- return {
166
- systemPrompt: `\n\nIMPORTANT: You are restricted to working within the directory: ${workspaceDir}
167
- Do not access files, run commands, or reference paths outside this directory.`,
168
- };
169
- });
170
- };
171
- }
package/src/service.ts DELETED
@@ -1,374 +0,0 @@
1
- /**
2
- * clankie service installer — manages systemd (Linux) and launchd (macOS) services.
3
- *
4
- * Commands:
5
- * clankie daemon install — install and start the service
6
- * clankie daemon uninstall — stop and remove the service
7
- * clankie daemon logs — show service logs
8
- *
9
- * On Linux: installs a systemd user service (~/.config/systemd/user/clankie.service)
10
- * On macOS: installs a launchd user agent (~/Library/LaunchAgents/ai.clankie.daemon.plist)
11
- *
12
- * Note: Requires Node 24+ for native TypeScript execution.
13
- */
14
-
15
- import { execSync } from "node:child_process";
16
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
17
- import { homedir, platform } from "node:os";
18
- import { dirname, join } from "node:path";
19
- import { getAppDir } from "./config.ts";
20
-
21
- const SERVICE_NAME = "clankie";
22
- const LAUNCHD_LABEL = "ai.clankie.daemon";
23
-
24
- // ─── Resolve the app binary path ──────────────────────────────────────────────
25
-
26
- function _resolveAppBinary(): string {
27
- // If running from a compiled binary, use its path
28
- if (!process.argv[1]?.endsWith(".ts")) {
29
- return process.argv[0];
30
- }
31
-
32
- // Running from source — use bun + script path
33
- // Return the full command that systemd/launchd will use
34
- return process.argv[0]; // bun binary path
35
- }
36
-
37
- function resolveProgramArguments(): string[] {
38
- if (!process.argv[1]?.endsWith(".ts")) {
39
- // Compiled binary
40
- return [process.argv[0], "start", "--foreground"];
41
- }
42
- // Running from source with Node.js (TypeScript files)
43
- return [process.argv[0], process.argv[1], "start", "--foreground"];
44
- }
45
-
46
- // ─── Systemd (Linux) ──────────────────────────────────────────────────────────
47
-
48
- function systemdUnitPath(): string {
49
- return join(homedir(), ".config", "systemd", "user", `${SERVICE_NAME}.service`);
50
- }
51
-
52
- function buildSystemdUnit(): string {
53
- const args = resolveProgramArguments();
54
- const execStart = args.map(systemdEscapeArg).join(" ");
55
- const workspace = join(getAppDir(), "workspace");
56
- const logDir = join(getAppDir(), "logs");
57
-
58
- return [
59
- "[Unit]",
60
- `Description=clankie — personal AI assistant daemon`,
61
- "After=network-online.target",
62
- "Wants=network-online.target",
63
- "",
64
- "[Service]",
65
- `ExecStart=${execStart}`,
66
- `WorkingDirectory=${workspace}`,
67
- "Restart=always",
68
- "RestartSec=5",
69
- "KillMode=process",
70
- `Environment=HOME=${homedir()}`,
71
- `Environment=PATH=${process.env.PATH}`,
72
- `StandardOutput=append:${join(logDir, "daemon.log")}`,
73
- `StandardError=append:${join(logDir, "daemon.log")}`,
74
- "",
75
- "[Install]",
76
- "WantedBy=default.target",
77
- "",
78
- ].join("\n");
79
- }
80
-
81
- function systemdEscapeArg(value: string): string {
82
- if (!/[\s"\\]/.test(value)) return value;
83
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
84
- }
85
-
86
- function execSafe(cmd: string): { ok: boolean; stdout: string; stderr: string } {
87
- try {
88
- const stdout = execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
89
- return { ok: true, stdout: stdout.trim(), stderr: "" };
90
- } catch (err: unknown) {
91
- const error = err as { stdout?: string; stderr?: string };
92
- return { ok: false, stdout: error.stdout?.trim() ?? "", stderr: error.stderr?.trim() ?? "" };
93
- }
94
- }
95
-
96
- async function installSystemd(): Promise<void> {
97
- // Check systemctl is available
98
- const check = execSafe("systemctl --user status");
99
- if (!check.ok) {
100
- const detail = `${check.stderr} ${check.stdout}`.toLowerCase();
101
- if (detail.includes("not found") || detail.includes("no such file")) {
102
- console.error("systemctl not found. systemd user services are required on Linux.");
103
- process.exit(1);
104
- }
105
- }
106
-
107
- const unitPath = systemdUnitPath();
108
- const logDir = join(getAppDir(), "logs");
109
- const workspace = join(getAppDir(), "workspace");
110
-
111
- // Ensure directories exist
112
- mkdirSync(dirname(unitPath), { recursive: true });
113
- mkdirSync(logDir, { recursive: true });
114
- mkdirSync(workspace, { recursive: true });
115
-
116
- // Write unit file
117
- const unit = buildSystemdUnit();
118
- writeFileSync(unitPath, unit, "utf-8");
119
- console.log(`Wrote systemd unit: ${unitPath}`);
120
-
121
- // Enable lingering so services run without an active login session
122
- const linger = execSafe("loginctl enable-linger");
123
- if (linger.ok) {
124
- console.log("Enabled user linger (service runs without active login).");
125
- }
126
-
127
- // Reload, enable, and start
128
- const reload = execSafe("systemctl --user daemon-reload");
129
- if (!reload.ok) {
130
- console.error(`daemon-reload failed: ${reload.stderr}`);
131
- process.exit(1);
132
- }
133
-
134
- const enable = execSafe(`systemctl --user enable ${SERVICE_NAME}.service`);
135
- if (!enable.ok) {
136
- console.error(`enable failed: ${enable.stderr}`);
137
- process.exit(1);
138
- }
139
-
140
- const restart = execSafe(`systemctl --user restart ${SERVICE_NAME}.service`);
141
- if (!restart.ok) {
142
- console.error(`restart failed: ${restart.stderr}`);
143
- process.exit(1);
144
- }
145
-
146
- console.log(`\n✓ Installed and started systemd service: ${SERVICE_NAME}.service`);
147
- console.log(` Logs: journalctl --user -u ${SERVICE_NAME} -f`);
148
- console.log(` Or: ${join(logDir, "daemon.log")}`);
149
- }
150
-
151
- async function uninstallSystemd(): Promise<void> {
152
- const unitPath = systemdUnitPath();
153
-
154
- execSafe(`systemctl --user disable --now ${SERVICE_NAME}.service`);
155
-
156
- try {
157
- unlinkSync(unitPath);
158
- console.log(`Removed: ${unitPath}`);
159
- } catch {
160
- console.log(`Service file not found at ${unitPath}`);
161
- }
162
-
163
- execSafe("systemctl --user daemon-reload");
164
- console.log(`✓ Uninstalled systemd service.`);
165
- }
166
-
167
- function logsSystemd(): void {
168
- const logFile = join(getAppDir(), "logs", "daemon.log");
169
- console.log(`Log file: ${logFile}\n`);
170
-
171
- // Try journalctl first, fall back to log file
172
- const result = execSafe(`journalctl --user -u ${SERVICE_NAME} --no-pager -n 50`);
173
- if (result.ok && result.stdout) {
174
- console.log(result.stdout);
175
- } else if (existsSync(logFile)) {
176
- const content = readFileSync(logFile, "utf-8");
177
- const lines = content.split("\n");
178
- const last50 = lines.slice(-50).join("\n");
179
- console.log(last50);
180
- } else {
181
- console.log("No logs found.");
182
- }
183
- }
184
-
185
- function statusSystemd(): void {
186
- const result = execSafe(`systemctl --user status ${SERVICE_NAME}.service`);
187
- console.log(result.stdout || result.stderr || "Service not found.");
188
- }
189
-
190
- // ─── launchd (macOS) ──────────────────────────────────────────────────────────
191
-
192
- function launchdPlistPath(): string {
193
- return join(homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
194
- }
195
-
196
- function plistEscape(value: string): string {
197
- return value
198
- .replace(/&/g, "&amp;")
199
- .replace(/</g, "&lt;")
200
- .replace(/>/g, "&gt;")
201
- .replace(/"/g, "&quot;")
202
- .replace(/'/g, "&apos;");
203
- }
204
-
205
- function buildLaunchdPlist(): string {
206
- const args = resolveProgramArguments();
207
- const logDir = join(getAppDir(), "logs");
208
- const workspace = join(getAppDir(), "workspace");
209
-
210
- const argsXml = args.map((a) => ` <string>${plistEscape(a)}</string>`).join("\n");
211
-
212
- // Build environment variables
213
- const envVars: Record<string, string> = {};
214
- if (process.env.PATH) envVars.PATH = process.env.PATH;
215
- if (process.env.HOME) envVars.HOME = process.env.HOME;
216
-
217
- const envXml = Object.entries(envVars)
218
- .map(([k, v]) => ` <key>${plistEscape(k)}</key>\n <string>${plistEscape(v)}</string>`)
219
- .join("\n");
220
-
221
- return `<?xml version="1.0" encoding="UTF-8"?>
222
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
223
- <plist version="1.0">
224
- <dict>
225
- <key>Label</key>
226
- <string>${plistEscape(LAUNCHD_LABEL)}</string>
227
- <key>RunAtLoad</key>
228
- <true/>
229
- <key>KeepAlive</key>
230
- <true/>
231
- <key>ProgramArguments</key>
232
- <array>
233
- ${argsXml}
234
- </array>
235
- <key>WorkingDirectory</key>
236
- <string>${plistEscape(workspace)}</string>
237
- <key>StandardOutPath</key>
238
- <string>${plistEscape(join(logDir, "daemon.log"))}</string>
239
- <key>StandardErrorPath</key>
240
- <string>${plistEscape(join(logDir, "daemon.log"))}</string>
241
- <key>EnvironmentVariables</key>
242
- <dict>
243
- ${envXml}
244
- </dict>
245
- </dict>
246
- </plist>
247
- `;
248
- }
249
-
250
- async function installLaunchd(): Promise<void> {
251
- const plistPath = launchdPlistPath();
252
- const logDir = join(getAppDir(), "logs");
253
- const workspace = join(getAppDir(), "workspace");
254
-
255
- mkdirSync(dirname(plistPath), { recursive: true });
256
- mkdirSync(logDir, { recursive: true });
257
- mkdirSync(workspace, { recursive: true });
258
-
259
- // Unload existing if present
260
- if (existsSync(plistPath)) {
261
- execSafe(`launchctl unload "${plistPath}"`);
262
- }
263
-
264
- const plist = buildLaunchdPlist();
265
- writeFileSync(plistPath, plist, "utf-8");
266
- console.log(`Wrote plist: ${plistPath}`);
267
-
268
- const load = execSafe(`launchctl load "${plistPath}"`);
269
- if (!load.ok) {
270
- console.error(`launchctl load failed: ${load.stderr}`);
271
- process.exit(1);
272
- }
273
-
274
- console.log(`\n✓ Installed and started launchd agent: ${LAUNCHD_LABEL}`);
275
- console.log(` Logs: tail -f ${join(logDir, "daemon.log")}`);
276
- }
277
-
278
- async function uninstallLaunchd(): Promise<void> {
279
- const plistPath = launchdPlistPath();
280
-
281
- if (existsSync(plistPath)) {
282
- execSafe(`launchctl unload "${plistPath}"`);
283
- unlinkSync(plistPath);
284
- console.log(`Removed: ${plistPath}`);
285
- } else {
286
- console.log(`Plist not found at ${plistPath}`);
287
- }
288
-
289
- console.log(`✓ Uninstalled launchd agent.`);
290
- }
291
-
292
- function logsLaunchd(): void {
293
- const logFile = join(getAppDir(), "logs", "daemon.log");
294
- console.log(`Log file: ${logFile}\n`);
295
-
296
- if (existsSync(logFile)) {
297
- const content = readFileSync(logFile, "utf-8");
298
- const lines = content.split("\n");
299
- const last50 = lines.slice(-50).join("\n");
300
- console.log(last50);
301
- } else {
302
- console.log("No logs found.");
303
- }
304
- }
305
-
306
- function statusLaunchd(): void {
307
- const result = execSafe(`launchctl list | grep ${LAUNCHD_LABEL}`);
308
- if (result.ok && result.stdout) {
309
- const parts = result.stdout.split(/\s+/);
310
- const pid = parts[0];
311
- const exitCode = parts[1];
312
- if (pid && pid !== "-") {
313
- console.log(`Daemon is running (pid ${pid}).`);
314
- } else {
315
- console.log(`Daemon is not running (last exit code: ${exitCode}).`);
316
- }
317
- } else {
318
- console.log("Daemon is not installed as a launchd agent.");
319
- }
320
- }
321
-
322
- // ─── Platform dispatch ────────────────────────────────────────────────────────
323
-
324
- const isMac = platform() === "darwin";
325
- const isLinux = platform() === "linux";
326
-
327
- export async function installService(): Promise<void> {
328
- if (isMac) {
329
- await installLaunchd();
330
- } else if (isLinux) {
331
- await installSystemd();
332
- } else {
333
- console.error(`Service installation not supported on ${platform()}.`);
334
- console.log("Run 'clankie start' manually instead.");
335
- process.exit(1);
336
- }
337
- }
338
-
339
- export async function uninstallService(): Promise<void> {
340
- if (isMac) {
341
- await uninstallLaunchd();
342
- } else if (isLinux) {
343
- await uninstallSystemd();
344
- } else {
345
- console.error(`Service management not supported on ${platform()}.`);
346
- process.exit(1);
347
- }
348
- }
349
-
350
- export function showServiceLogs(): void {
351
- if (isMac) {
352
- logsLaunchd();
353
- } else if (isLinux) {
354
- logsSystemd();
355
- } else {
356
- const logFile = join(getAppDir(), "logs", "daemon.log");
357
- if (existsSync(logFile)) {
358
- console.log(readFileSync(logFile, "utf-8"));
359
- } else {
360
- console.log("No logs found.");
361
- }
362
- }
363
- }
364
-
365
- export function showServiceStatus(): void {
366
- if (isMac) {
367
- statusLaunchd();
368
- } else if (isLinux) {
369
- statusSystemd();
370
- } else {
371
- console.log(`Service management not supported on ${platform()}.`);
372
- console.log("Use 'clankie status' to check if the daemon is running via PID file.");
373
- }
374
- }