codepiper 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.
Files changed (149) hide show
  1. package/.env.example +28 -0
  2. package/CHANGELOG.md +10 -0
  3. package/LEGAL_NOTICE.md +39 -0
  4. package/LICENSE +21 -0
  5. package/README.md +524 -0
  6. package/package.json +90 -0
  7. package/packages/cli/package.json +13 -0
  8. package/packages/cli/src/commands/analytics.ts +157 -0
  9. package/packages/cli/src/commands/attach.ts +299 -0
  10. package/packages/cli/src/commands/audit.ts +50 -0
  11. package/packages/cli/src/commands/auth.ts +261 -0
  12. package/packages/cli/src/commands/daemon.ts +162 -0
  13. package/packages/cli/src/commands/doctor.ts +303 -0
  14. package/packages/cli/src/commands/env-set.ts +162 -0
  15. package/packages/cli/src/commands/hook-forward.ts +268 -0
  16. package/packages/cli/src/commands/keys.ts +77 -0
  17. package/packages/cli/src/commands/kill.ts +19 -0
  18. package/packages/cli/src/commands/logs.ts +419 -0
  19. package/packages/cli/src/commands/model.ts +172 -0
  20. package/packages/cli/src/commands/policy-set.ts +185 -0
  21. package/packages/cli/src/commands/policy.ts +227 -0
  22. package/packages/cli/src/commands/providers.ts +114 -0
  23. package/packages/cli/src/commands/resize.ts +34 -0
  24. package/packages/cli/src/commands/send.ts +184 -0
  25. package/packages/cli/src/commands/sessions.ts +202 -0
  26. package/packages/cli/src/commands/slash.ts +92 -0
  27. package/packages/cli/src/commands/start.ts +243 -0
  28. package/packages/cli/src/commands/stop.ts +19 -0
  29. package/packages/cli/src/commands/tail.ts +137 -0
  30. package/packages/cli/src/commands/workflow.ts +786 -0
  31. package/packages/cli/src/commands/workspace.ts +127 -0
  32. package/packages/cli/src/lib/api.ts +78 -0
  33. package/packages/cli/src/lib/args.ts +72 -0
  34. package/packages/cli/src/lib/format.ts +93 -0
  35. package/packages/cli/src/main.ts +563 -0
  36. package/packages/core/package.json +7 -0
  37. package/packages/core/src/config.ts +30 -0
  38. package/packages/core/src/errors.ts +38 -0
  39. package/packages/core/src/eventBus.ts +56 -0
  40. package/packages/core/src/eventBusAdapter.ts +143 -0
  41. package/packages/core/src/index.ts +10 -0
  42. package/packages/core/src/sqliteEventBus.ts +336 -0
  43. package/packages/core/src/types.ts +63 -0
  44. package/packages/daemon/package.json +11 -0
  45. package/packages/daemon/src/api/analyticsRoutes.ts +343 -0
  46. package/packages/daemon/src/api/authRoutes.ts +344 -0
  47. package/packages/daemon/src/api/bodyLimit.ts +133 -0
  48. package/packages/daemon/src/api/envSetRoutes.ts +170 -0
  49. package/packages/daemon/src/api/gitRoutes.ts +409 -0
  50. package/packages/daemon/src/api/hooks.ts +588 -0
  51. package/packages/daemon/src/api/inputPolicy.ts +249 -0
  52. package/packages/daemon/src/api/notificationRoutes.ts +532 -0
  53. package/packages/daemon/src/api/policyRoutes.ts +234 -0
  54. package/packages/daemon/src/api/policySetRoutes.ts +445 -0
  55. package/packages/daemon/src/api/routeUtils.ts +28 -0
  56. package/packages/daemon/src/api/routes.ts +1004 -0
  57. package/packages/daemon/src/api/server.ts +1388 -0
  58. package/packages/daemon/src/api/settingsRoutes.ts +367 -0
  59. package/packages/daemon/src/api/sqliteErrors.ts +47 -0
  60. package/packages/daemon/src/api/stt.ts +143 -0
  61. package/packages/daemon/src/api/terminalRoutes.ts +200 -0
  62. package/packages/daemon/src/api/validation.ts +287 -0
  63. package/packages/daemon/src/api/validationRoutes.ts +174 -0
  64. package/packages/daemon/src/api/workflowRoutes.ts +567 -0
  65. package/packages/daemon/src/api/workspaceRoutes.ts +151 -0
  66. package/packages/daemon/src/api/ws.ts +1588 -0
  67. package/packages/daemon/src/auth/apiRateLimiter.ts +73 -0
  68. package/packages/daemon/src/auth/authMiddleware.ts +305 -0
  69. package/packages/daemon/src/auth/authService.ts +496 -0
  70. package/packages/daemon/src/auth/rateLimiter.ts +137 -0
  71. package/packages/daemon/src/config/pricing.ts +79 -0
  72. package/packages/daemon/src/crypto/encryption.ts +196 -0
  73. package/packages/daemon/src/db/db.ts +2745 -0
  74. package/packages/daemon/src/db/index.ts +16 -0
  75. package/packages/daemon/src/db/migrations.ts +182 -0
  76. package/packages/daemon/src/db/policyDb.ts +349 -0
  77. package/packages/daemon/src/db/schema.sql +408 -0
  78. package/packages/daemon/src/db/workflowDb.ts +464 -0
  79. package/packages/daemon/src/git/gitUtils.ts +544 -0
  80. package/packages/daemon/src/index.ts +6 -0
  81. package/packages/daemon/src/main.ts +525 -0
  82. package/packages/daemon/src/notifications/pushNotifier.ts +369 -0
  83. package/packages/daemon/src/providers/codexAppServerScaffold.ts +49 -0
  84. package/packages/daemon/src/providers/registry.ts +111 -0
  85. package/packages/daemon/src/providers/types.ts +82 -0
  86. package/packages/daemon/src/sessions/auditLogger.ts +103 -0
  87. package/packages/daemon/src/sessions/policyEngine.ts +165 -0
  88. package/packages/daemon/src/sessions/policyMatcher.ts +114 -0
  89. package/packages/daemon/src/sessions/policyTypes.ts +94 -0
  90. package/packages/daemon/src/sessions/ptyProcess.ts +141 -0
  91. package/packages/daemon/src/sessions/sessionManager.ts +1770 -0
  92. package/packages/daemon/src/sessions/tmuxSession.ts +1073 -0
  93. package/packages/daemon/src/sessions/transcriptManager.ts +110 -0
  94. package/packages/daemon/src/sessions/transcriptParser.ts +149 -0
  95. package/packages/daemon/src/sessions/transcriptTailer.ts +214 -0
  96. package/packages/daemon/src/tracking/tokenTracker.ts +168 -0
  97. package/packages/daemon/src/workflows/contextManager.ts +83 -0
  98. package/packages/daemon/src/workflows/index.ts +31 -0
  99. package/packages/daemon/src/workflows/resultExtractor.ts +118 -0
  100. package/packages/daemon/src/workflows/waitConditionPoller.ts +131 -0
  101. package/packages/daemon/src/workflows/workflowParser.ts +217 -0
  102. package/packages/daemon/src/workflows/workflowRunner.ts +969 -0
  103. package/packages/daemon/src/workflows/workflowTypes.ts +188 -0
  104. package/packages/daemon/src/workflows/workflowValidator.ts +533 -0
  105. package/packages/providers/claude-code/package.json +11 -0
  106. package/packages/providers/claude-code/src/index.ts +7 -0
  107. package/packages/providers/claude-code/src/overlaySettings.ts +198 -0
  108. package/packages/providers/claude-code/src/provider.ts +311 -0
  109. package/packages/web/dist/android-chrome-192x192.png +0 -0
  110. package/packages/web/dist/android-chrome-512x512.png +0 -0
  111. package/packages/web/dist/apple-touch-icon.png +0 -0
  112. package/packages/web/dist/assets/AnalyticsPage-BIopKWRf.js +17 -0
  113. package/packages/web/dist/assets/PoliciesPage-CjdLN3dl.js +11 -0
  114. package/packages/web/dist/assets/SessionDetailPage-BtSA0V0M.js +179 -0
  115. package/packages/web/dist/assets/SettingsPage-Dbbz4Ca5.js +37 -0
  116. package/packages/web/dist/assets/WorkflowsPage-Dv6f3GgU.js +1 -0
  117. package/packages/web/dist/assets/chart-vendor-DlOHLaCG.js +49 -0
  118. package/packages/web/dist/assets/codicon-ngg6Pgfi.ttf +0 -0
  119. package/packages/web/dist/assets/css.worker-BvV5MPou.js +93 -0
  120. package/packages/web/dist/assets/editor.worker-CKy7Pnvo.js +26 -0
  121. package/packages/web/dist/assets/html.worker-BLJhxQJQ.js +470 -0
  122. package/packages/web/dist/assets/index-BbdhRfr2.css +1 -0
  123. package/packages/web/dist/assets/index-hgphORiw.js +204 -0
  124. package/packages/web/dist/assets/json.worker-usMZ-FED.js +58 -0
  125. package/packages/web/dist/assets/monaco-core-B_19GPAS.css +1 -0
  126. package/packages/web/dist/assets/monaco-core-DQ5Mk8AK.js +1234 -0
  127. package/packages/web/dist/assets/monaco-react-DfZNWvtW.js +11 -0
  128. package/packages/web/dist/assets/monacoSetup-DvBj52bT.js +1 -0
  129. package/packages/web/dist/assets/pencil-Dbczxz59.js +11 -0
  130. package/packages/web/dist/assets/react-vendor-B5MgMUHH.js +136 -0
  131. package/packages/web/dist/assets/refresh-cw-B0MGsYPL.js +6 -0
  132. package/packages/web/dist/assets/tabs-C8LsWiR5.js +1 -0
  133. package/packages/web/dist/assets/terminal-vendor-Cs8KPbV3.js +9 -0
  134. package/packages/web/dist/assets/terminal-vendor-LcAfv9l9.css +32 -0
  135. package/packages/web/dist/assets/trash-2-Btlg0d4l.js +6 -0
  136. package/packages/web/dist/assets/ts.worker-DGHjMaqB.js +67731 -0
  137. package/packages/web/dist/favicon.ico +0 -0
  138. package/packages/web/dist/icon.svg +1 -0
  139. package/packages/web/dist/index.html +29 -0
  140. package/packages/web/dist/manifest.json +29 -0
  141. package/packages/web/dist/og-image.png +0 -0
  142. package/packages/web/dist/originals/android-chrome-192x192.png +0 -0
  143. package/packages/web/dist/originals/android-chrome-512x512.png +0 -0
  144. package/packages/web/dist/originals/apple-touch-icon.png +0 -0
  145. package/packages/web/dist/originals/favicon.ico +0 -0
  146. package/packages/web/dist/piper.svg +1 -0
  147. package/packages/web/dist/sounds/codepiper-soft-chime.wav +0 -0
  148. package/packages/web/dist/sw.js +257 -0
  149. package/scripts/postinstall-link-workspaces.mjs +58 -0
package/package.json ADDED
@@ -0,0 +1,90 @@
1
+ {
2
+ "name": "codepiper",
3
+ "version": "0.1.0",
4
+ "description": "CodePiper — Multi-session CLI orchestrator for Claude Code",
5
+ "bin": {
6
+ "codepiper": "./packages/cli/src/main.ts"
7
+ },
8
+ "files": [
9
+ "README.md",
10
+ "CHANGELOG.md",
11
+ "LICENSE",
12
+ "LEGAL_NOTICE.md",
13
+ ".env.example",
14
+ "package.json",
15
+ "scripts/postinstall-link-workspaces.mjs",
16
+ "packages/cli/package.json",
17
+ "packages/cli/src/**/*.ts",
18
+ "!packages/cli/src/**/*.test.ts",
19
+ "packages/core/package.json",
20
+ "packages/core/src/**/*.ts",
21
+ "!packages/core/src/**/*.test.ts",
22
+ "packages/daemon/package.json",
23
+ "packages/daemon/src/**/*.ts",
24
+ "packages/daemon/src/**/*.sql",
25
+ "!packages/daemon/src/**/*.test.ts",
26
+ "!packages/daemon/src/**/*.example.ts",
27
+ "!packages/daemon/src/**/example.ts",
28
+ "!packages/daemon/src/**/demo.ts",
29
+ "packages/providers/claude-code/package.json",
30
+ "packages/providers/claude-code/src/**/*.ts",
31
+ "!packages/providers/claude-code/src/**/*.test.ts",
32
+ "packages/web/dist/**"
33
+ ],
34
+ "workspaces": [
35
+ "packages/*",
36
+ "packages/providers/*"
37
+ ],
38
+ "scripts": {
39
+ "daemon": "bun run packages/daemon/src/main.ts",
40
+ "daemon:web": "bun run packages/daemon/src/main.ts --web",
41
+ "build:web": "bun run --cwd packages/web build",
42
+ "dev:web": "bun run --cwd packages/web dev",
43
+ "cli": "bun run packages/cli/src/main.ts",
44
+ "test": "bun test",
45
+ "test:integration": "bun test --integration",
46
+ "typecheck": "bunx tsc -b tsconfig.json",
47
+ "typecheck:strict:daemon": "bunx tsc -p packages/daemon/tsconfig.strict.json --pretty false",
48
+ "typecheck:strict:provider": "bunx tsc -p packages/providers/claude-code/tsconfig.strict.json --pretty false",
49
+ "typecheck:strict": "bun run typecheck:strict:daemon && bun run typecheck:strict:provider",
50
+ "release:smoke": "bash scripts/release-smoke.sh",
51
+ "bench:ws-transport": "bun run scripts/bench/ws-transport-bench.ts",
52
+ "bench:ws-transport:artifact": "bun run scripts/bench/ws-transport-bench.ts --json --artifact-dir benchmarks/ws-transport",
53
+ "bench:ws-transport:bun-pty-prototype": "bun run scripts/bench/ws-transport-bench.ts --bun-pty-prototype",
54
+ "security:deps": "node scripts/security/check-deps.mjs",
55
+ "security:secrets": "node scripts/security/check-secrets.mjs",
56
+ "security:check": "bun run security:secrets && bun run security:deps",
57
+ "pack:check": "node scripts/check-packaging.mjs",
58
+ "pack:check:fast": "node scripts/check-packaging.mjs --skip-build",
59
+ "pack:smoke": "node scripts/pack-runtime-smoke.mjs",
60
+ "pack:smoke:fast": "node scripts/pack-runtime-smoke.mjs --skip-build",
61
+ "prepack": "node scripts/check-packaging.mjs --prepack",
62
+ "postinstall": "node scripts/postinstall-link-workspaces.mjs",
63
+ "lint": "biome check .",
64
+ "lint:fix": "biome check --write .",
65
+ "format": "biome format --write .",
66
+ "format:check": "biome format .",
67
+ "check": "bun run format:check && bun run lint && bun run typecheck && bun run typecheck:strict && bun test",
68
+ "pre-commit": "bun run lint:fix && bun run typecheck && bun run typecheck:strict && bun test"
69
+ },
70
+ "devDependencies": {
71
+ "@biomejs/biome": "^2.4.2",
72
+ "@types/bun": "latest",
73
+ "@types/js-yaml": "^4.0.9",
74
+ "@types/qrcode": "^1.5.6",
75
+ "@types/web-push": "^3.6.4",
76
+ "typescript": "5.9.3"
77
+ },
78
+ "engines": {
79
+ "bun": ">=1.3.5"
80
+ },
81
+ "dependencies": {
82
+ "@types/micromatch": "^4.0.10",
83
+ "js-yaml": "^4.1.1",
84
+ "jsonpath-plus": "^10.3.0",
85
+ "micromatch": "^4.0.8",
86
+ "otpauth": "^9.5.0",
87
+ "qrcode": "^1.5.4",
88
+ "web-push": "^3.6.7"
89
+ }
90
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "@codepiper/cli",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./src/main.ts",
6
+ "types": "./src/main.ts",
7
+ "dependencies": {
8
+ "@codepiper/core": "workspace:*"
9
+ },
10
+ "devDependencies": {
11
+ "@types/bun": "latest"
12
+ }
13
+ }
@@ -0,0 +1,157 @@
1
+ import { daemonJson } from "../lib/api";
2
+ import { getOption, getSocket } from "../lib/args";
3
+ import { colors, info, table } from "../lib/format";
4
+
5
+ function daysToRange(days: string): string {
6
+ switch (days) {
7
+ case "1":
8
+ return "today";
9
+ case "30":
10
+ return "30d";
11
+ case "0":
12
+ return "all";
13
+ default:
14
+ return "7d";
15
+ }
16
+ }
17
+
18
+ async function showOverview(socket: string, range: string): Promise<void> {
19
+ const data = await daemonJson<{
20
+ sessionsCount: number;
21
+ activeSessions: number;
22
+ totalTokens: number;
23
+ totalMessages: number;
24
+ cacheHitRate: number;
25
+ costEstimate: number;
26
+ }>(`/analytics/overview?range=${range}`, { socket });
27
+
28
+ console.log(`${colors.bold}Analytics Overview${colors.reset} (range: ${range})\n`);
29
+ info("Total sessions", String(data.sessionsCount ?? 0));
30
+ info("Active sessions", String(data.activeSessions ?? 0));
31
+ info("Total tokens", (data.totalTokens ?? 0).toLocaleString());
32
+ info("Total messages", String(data.totalMessages ?? 0));
33
+ info("Cache hit rate", `${data.cacheHitRate ?? 0}%`);
34
+ if (data.costEstimate !== undefined) {
35
+ info("Estimated cost", `$${data.costEstimate.toFixed(2)}`);
36
+ }
37
+ }
38
+
39
+ async function showTokens(socket: string, range: string): Promise<void> {
40
+ const data = await daemonJson<
41
+ Array<{
42
+ model: string;
43
+ tokens: number;
44
+ prompt_tokens: number;
45
+ completion_tokens: number;
46
+ cache_read: number;
47
+ cache_creation: number;
48
+ requests: number;
49
+ costEstimate: number;
50
+ }>
51
+ >(`/analytics/tokens-by-model?range=${range}`, { socket });
52
+
53
+ console.log(`${colors.bold}Tokens by Model${colors.reset} (range: ${range})\n`);
54
+
55
+ if (!data || data.length === 0) {
56
+ console.log("No token data available.");
57
+ return;
58
+ }
59
+
60
+ const rows = data.map((m) => [
61
+ m.model || "unknown",
62
+ (m.tokens ?? 0).toLocaleString(),
63
+ (m.prompt_tokens ?? 0).toLocaleString(),
64
+ (m.completion_tokens ?? 0).toLocaleString(),
65
+ String(m.requests ?? 0),
66
+ m.costEstimate !== undefined ? `$${m.costEstimate.toFixed(2)}` : "-",
67
+ ]);
68
+
69
+ console.log(table(["MODEL", "TOTAL", "PROMPT", "COMPLETION", "REQUESTS", "COST"], rows));
70
+ }
71
+
72
+ async function showTools(socket: string, range: string): Promise<void> {
73
+ const data = await daemonJson<Array<{ tool: string; count: number }>>(
74
+ `/analytics/tool-usage?range=${range}`,
75
+ { socket }
76
+ );
77
+
78
+ console.log(`${colors.bold}Tool Usage${colors.reset} (range: ${range})\n`);
79
+
80
+ if (!data || data.length === 0) {
81
+ console.log("No tool usage data available.");
82
+ return;
83
+ }
84
+
85
+ const rows = data.map((t) => [t.tool || "-", String(t.count ?? 0)]);
86
+
87
+ console.log(table(["TOOL", "USES"], rows));
88
+ }
89
+
90
+ async function showActivity(socket: string, range: string): Promise<void> {
91
+ const data = await daemonJson<
92
+ Array<{
93
+ date: string;
94
+ user_messages: number;
95
+ assistant_messages: number;
96
+ total: number;
97
+ }>
98
+ >(`/analytics/activity-timeline?range=${range}`, { socket });
99
+
100
+ console.log(`${colors.bold}Activity Timeline${colors.reset} (range: ${range})\n`);
101
+
102
+ if (!data || data.length === 0) {
103
+ console.log("No activity data available.");
104
+ return;
105
+ }
106
+
107
+ const rows = data.map((a) => [
108
+ a.date || "-",
109
+ String(a.user_messages ?? 0),
110
+ String(a.assistant_messages ?? 0),
111
+ String(a.total ?? 0),
112
+ ]);
113
+
114
+ console.log(table(["DATE", "USER", "ASSISTANT", "TOTAL"], rows));
115
+ }
116
+
117
+ async function showSessions(socket: string, range: string): Promise<void> {
118
+ const data = await daemonJson<Array<{ provider: string; count: number }>>(
119
+ `/analytics/sessions-by-provider?range=${range}`,
120
+ { socket }
121
+ );
122
+
123
+ console.log(`${colors.bold}Sessions by Provider${colors.reset} (range: ${range})\n`);
124
+
125
+ if (!data || data.length === 0) {
126
+ console.log("No session data available.");
127
+ return;
128
+ }
129
+
130
+ const rows = data.map((s) => [s.provider || "-", String(s.count ?? 0)]);
131
+
132
+ console.log(table(["PROVIDER", "SESSIONS"], rows));
133
+ }
134
+
135
+ export async function runAnalyticsCommand(args: string[]): Promise<void> {
136
+ const socket = getSocket(args);
137
+ const days = getOption(args, "days") || "7";
138
+ const range = daysToRange(days);
139
+ const subcommand = args.find((a) => !a.startsWith("-")) || "overview";
140
+
141
+ switch (subcommand) {
142
+ case "overview":
143
+ return showOverview(socket, range);
144
+ case "tokens":
145
+ return showTokens(socket, range);
146
+ case "tools":
147
+ return showTools(socket, range);
148
+ case "activity":
149
+ return showActivity(socket, range);
150
+ case "sessions":
151
+ return showSessions(socket, range);
152
+ default:
153
+ throw new Error(
154
+ `Unknown analytics subcommand: ${subcommand}. Use: overview, tokens, tools, activity, sessions`
155
+ );
156
+ }
157
+ }
@@ -0,0 +1,299 @@
1
+ import * as os from "node:os";
2
+ import { readErrorJson, readJson, responseErrorMessage } from "../lib/api";
3
+ import { getRequiredValue } from "../lib/args";
4
+
5
+ export interface AttachOptions {
6
+ sessionId: string;
7
+ socket: string;
8
+ follow: boolean;
9
+ }
10
+
11
+ export function parseAttachOptions(args: string[]): AttachOptions {
12
+ let sessionId: string | undefined;
13
+ let socket = "/tmp/codepiper.sock";
14
+ let follow = false;
15
+
16
+ for (let i = 0; i < args.length; i++) {
17
+ const arg = args[i];
18
+ if (arg === undefined) {
19
+ continue;
20
+ }
21
+
22
+ if (arg === "--socket" || arg === "-s") {
23
+ socket = getRequiredValue(args, i, arg);
24
+ i++;
25
+ } else if (arg === "--follow" || arg === "-f") {
26
+ follow = true;
27
+ } else if (!(arg.startsWith("-") || sessionId)) {
28
+ sessionId = arg;
29
+ }
30
+ }
31
+
32
+ if (!sessionId) {
33
+ throw new Error("session-id is required");
34
+ }
35
+
36
+ return { sessionId, socket, follow };
37
+ }
38
+
39
+ interface TerminalState {
40
+ originalRawMode: boolean;
41
+ originalIsTTY: boolean;
42
+ }
43
+
44
+ class SessionOutputUnavailableError extends Error {}
45
+
46
+ function getHomeDir(): string {
47
+ return process.env.HOME || os.homedir();
48
+ }
49
+
50
+ export async function verifySessionExists(sessionId: string, socket: string): Promise<void> {
51
+ try {
52
+ const response = await fetch(`http://localhost/sessions/${sessionId}`, {
53
+ unix: socket,
54
+ method: "GET",
55
+ });
56
+
57
+ if (!response.ok) {
58
+ if (response.status === 404) {
59
+ throw new Error(`Session not found: ${sessionId}`);
60
+ }
61
+ const errorData = await readErrorJson(response);
62
+ throw new Error(responseErrorMessage(response, errorData));
63
+ }
64
+ } catch (error: any) {
65
+ if (error.code === "ENOENT" || error.message?.includes("ENOENT")) {
66
+ throw new Error(`Failed to connect to daemon at ${socket}. Is the daemon running?`);
67
+ }
68
+ throw error;
69
+ }
70
+ }
71
+
72
+ async function displayHistoricalOutput(sessionId: string): Promise<void> {
73
+ // Try to read historical output from log file
74
+ const outputLogPath = `${getHomeDir()}/.codepiper/sessions/${sessionId}/output.log`;
75
+
76
+ try {
77
+ const fs = require("node:fs");
78
+ const content = await fs.promises.readFile(outputLogPath, "utf-8");
79
+ if (content) {
80
+ process.stdout.write(content);
81
+ }
82
+ } catch (_err) {
83
+ // Log file doesn't exist yet, or is empty - that's okay
84
+ }
85
+ }
86
+
87
+ async function setupTerminal(follow: boolean): Promise<TerminalState> {
88
+ const originalRawMode = process.stdin.isRaw ?? false;
89
+ const originalIsTTY = process.stdin.isTTY ?? false;
90
+
91
+ if (!follow && originalIsTTY) {
92
+ // Interactive mode: enable raw mode for direct key forwarding
93
+ process.stdin.setRawMode(true);
94
+ process.stdin.resume();
95
+ }
96
+
97
+ return {
98
+ originalRawMode,
99
+ originalIsTTY,
100
+ };
101
+ }
102
+
103
+ async function restoreTerminal(state: TerminalState): Promise<void> {
104
+ if (process.stdin.setRawMode && state.originalIsTTY) {
105
+ process.stdin.setRawMode(state.originalRawMode);
106
+ }
107
+ if (!state.originalRawMode) {
108
+ process.stdin.pause();
109
+ }
110
+ }
111
+
112
+ export async function fetchSessionOutput(sessionId: string, socket: string): Promise<string> {
113
+ try {
114
+ const response = await fetch(`http://localhost/sessions/${sessionId}/output`, {
115
+ unix: socket,
116
+ method: "GET",
117
+ });
118
+
119
+ if (!response.ok) {
120
+ const errorData = await readErrorJson(response);
121
+ const message = responseErrorMessage(response, errorData);
122
+ if (response.status === 404 || response.status === 409) {
123
+ throw new SessionOutputUnavailableError(message);
124
+ }
125
+ throw new Error(message);
126
+ }
127
+
128
+ const data = await readJson<{ output?: string }>(response);
129
+ return typeof data.output === "string" ? data.output : "";
130
+ } catch (error: any) {
131
+ if (error.code === "ENOENT" || error.message?.includes("ENOENT")) {
132
+ throw new Error(`Failed to connect to daemon at ${socket}. Is the daemon running?`);
133
+ }
134
+ throw error;
135
+ }
136
+ }
137
+
138
+ export function writeOutputDelta(previous: string, current: string): void {
139
+ if (current.length === 0 || current === previous) {
140
+ return;
141
+ }
142
+
143
+ if (!previous) {
144
+ process.stdout.write(current);
145
+ return;
146
+ }
147
+
148
+ // Fast path for append-only changes.
149
+ if (current.startsWith(previous)) {
150
+ process.stdout.write(current.slice(previous.length));
151
+ return;
152
+ }
153
+
154
+ // Fallback: emit changed suffix from common prefix.
155
+ const max = Math.min(previous.length, current.length);
156
+ let idx = 0;
157
+ while (idx < max && previous[idx] === current[idx]) {
158
+ idx++;
159
+ }
160
+ process.stdout.write(current.slice(idx));
161
+ }
162
+
163
+ async function connectAndStream(sessionId: string, socket: string, follow: boolean): Promise<void> {
164
+ return new Promise((resolve, reject) => {
165
+ let isClosing = false;
166
+ let polling = false;
167
+ let lastOutput = "";
168
+ let pollTimer: Timer | undefined;
169
+
170
+ const stop = (finish: "resolve" | "reject", error?: unknown) => {
171
+ if (isClosing) {
172
+ return;
173
+ }
174
+ isClosing = true;
175
+
176
+ if (pollTimer) {
177
+ clearInterval(pollTimer);
178
+ pollTimer = undefined;
179
+ }
180
+ process.off("SIGINT", sigintHandler);
181
+
182
+ if (!follow) {
183
+ process.stdin.off("data", stdinHandler);
184
+ }
185
+
186
+ if (finish === "resolve") {
187
+ resolve();
188
+ } else {
189
+ reject(error);
190
+ }
191
+ };
192
+
193
+ const pollOutput = async () => {
194
+ if (isClosing || polling) {
195
+ return;
196
+ }
197
+ polling = true;
198
+
199
+ try {
200
+ const output = await fetchSessionOutput(sessionId, socket);
201
+ if (!isClosing) {
202
+ writeOutputDelta(lastOutput, output);
203
+ lastOutput = output;
204
+ }
205
+ } catch (error) {
206
+ if (!isClosing) {
207
+ if (error instanceof SessionOutputUnavailableError) {
208
+ stop("resolve");
209
+ return;
210
+ }
211
+ stop("reject", error);
212
+ }
213
+ } finally {
214
+ polling = false;
215
+ }
216
+ };
217
+
218
+ // Handle stdin in interactive mode
219
+ const stdinHandler = async (chunk: Buffer) => {
220
+ const text = chunk.toString();
221
+
222
+ // Check for Ctrl+C (0x03)
223
+ if (text === "\x03") {
224
+ stop("resolve");
225
+ return;
226
+ }
227
+
228
+ // Echo input locally in raw mode (so user can see what they're typing)
229
+ // Output polling will also reflect terminal state.
230
+ process.stdout.write(text);
231
+
232
+ // Send input to session
233
+ try {
234
+ await fetch(`http://localhost/sessions/${sessionId}/send`, {
235
+ unix: socket,
236
+ method: "POST",
237
+ headers: {
238
+ "Content-Type": "application/json",
239
+ },
240
+ body: JSON.stringify({
241
+ text,
242
+ newline: false,
243
+ }),
244
+ });
245
+ } catch (err) {
246
+ if (!isClosing) {
247
+ stop("reject", err);
248
+ }
249
+ }
250
+ };
251
+
252
+ if (!follow) {
253
+ process.stdin.on("data", stdinHandler);
254
+ }
255
+
256
+ // Handle SIGINT (Ctrl+C from terminal, not from stdin in raw mode)
257
+ const sigintHandler = () => {
258
+ if (!isClosing) {
259
+ stop("resolve");
260
+ }
261
+ };
262
+
263
+ process.on("SIGINT", sigintHandler);
264
+
265
+ pollTimer = setInterval(() => {
266
+ void pollOutput();
267
+ }, 100);
268
+ void pollOutput();
269
+ });
270
+ }
271
+
272
+ export async function attachToSession(options: AttachOptions): Promise<void> {
273
+ // 1. Verify session exists
274
+ await verifySessionExists(options.sessionId, options.socket);
275
+
276
+ console.log(`Attaching to session ${options.sessionId}...`);
277
+ console.log(`Mode: ${options.follow ? "follow (read-only)" : "interactive"}`);
278
+ console.log(`Press Ctrl+C to detach.\n`);
279
+
280
+ // 2. Setup terminal
281
+ const terminalState = await setupTerminal(options.follow);
282
+
283
+ try {
284
+ // 3. Display historical output first
285
+ await displayHistoricalOutput(options.sessionId);
286
+
287
+ // 4. Connect and stream new output
288
+ await connectAndStream(options.sessionId, options.socket, options.follow);
289
+ } finally {
290
+ // 5. Always restore terminal state
291
+ await restoreTerminal(terminalState);
292
+ console.log("\nDetached.");
293
+ }
294
+ }
295
+
296
+ export async function runAttachCommand(args: string[]): Promise<void> {
297
+ const options = parseAttachOptions(args);
298
+ await attachToSession(options);
299
+ }
@@ -0,0 +1,50 @@
1
+ import { daemonJson } from "../lib/api";
2
+ import { getOption, getSocket } from "../lib/args";
3
+ import { colors, formatDate, table } from "../lib/format";
4
+
5
+ interface PolicyDecision {
6
+ id: number;
7
+ sessionId: string;
8
+ policyId?: number;
9
+ toolName: string;
10
+ decision: string;
11
+ reason?: string;
12
+ timestamp: string;
13
+ }
14
+
15
+ export async function runAuditCommand(args: string[]): Promise<void> {
16
+ const socket = getSocket(args);
17
+ const sessionId = getOption(args, "session");
18
+ const limit = getOption(args, "limit") || "50";
19
+
20
+ const params = new URLSearchParams();
21
+ if (sessionId) params.set("sessionId", sessionId);
22
+ params.set("limit", limit);
23
+
24
+ const query = params.toString();
25
+ const data = await daemonJson<{ decisions: PolicyDecision[] }>(`/policy-decisions?${query}`, {
26
+ socket,
27
+ });
28
+
29
+ if (data.decisions.length === 0) {
30
+ console.log("No policy decisions found.");
31
+ return;
32
+ }
33
+
34
+ const rows = data.decisions.map((d) => {
35
+ const decisionColor =
36
+ d.decision === "allow" ? colors.green : d.decision === "deny" ? colors.red : colors.yellow;
37
+
38
+ return [
39
+ formatDate(d.timestamp),
40
+ d.sessionId.slice(0, 8),
41
+ d.toolName,
42
+ `${decisionColor}${d.decision}${colors.reset}`,
43
+ d.policyId ? `#${d.policyId}` : `${colors.dim}-${colors.reset}`,
44
+ d.reason || `${colors.dim}-${colors.reset}`,
45
+ ];
46
+ });
47
+
48
+ console.log(table(["TIMESTAMP", "SESSION", "TOOL", "DECISION", "POLICY", "REASON"], rows));
49
+ console.log(`\n${colors.bold}Total:${colors.reset} ${data.decisions.length} decision(s)`);
50
+ }