@nifrajs/coding-agent 3.2.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 (211) hide show
  1. package/EXTENSIONS.md +25 -0
  2. package/PERFORMANCE.md +19 -0
  3. package/README.md +69 -0
  4. package/REGISTRY.md +15 -0
  5. package/SECURITY.md +32 -0
  6. package/dist/approvals.d.ts +70 -0
  7. package/dist/approvals.d.ts.map +1 -0
  8. package/dist/approvals.js +174 -0
  9. package/dist/approvals.js.map +1 -0
  10. package/dist/capabilities.d.ts +20 -0
  11. package/dist/capabilities.d.ts.map +1 -0
  12. package/dist/capabilities.js +49 -0
  13. package/dist/capabilities.js.map +1 -0
  14. package/dist/cli.d.ts +24 -0
  15. package/dist/cli.d.ts.map +1 -0
  16. package/dist/cli.js +380 -0
  17. package/dist/cli.js.map +1 -0
  18. package/dist/context.d.ts +17 -0
  19. package/dist/context.d.ts.map +1 -0
  20. package/dist/context.js +66 -0
  21. package/dist/context.js.map +1 -0
  22. package/dist/deployment-adapters.d.ts +40 -0
  23. package/dist/deployment-adapters.d.ts.map +1 -0
  24. package/dist/deployment-adapters.js +147 -0
  25. package/dist/deployment-adapters.js.map +1 -0
  26. package/dist/diff.d.ts +17 -0
  27. package/dist/diff.d.ts.map +1 -0
  28. package/dist/diff.js +61 -0
  29. package/dist/diff.js.map +1 -0
  30. package/dist/errors.d.ts +9 -0
  31. package/dist/errors.d.ts.map +1 -0
  32. package/dist/errors.js +35 -0
  33. package/dist/errors.js.map +1 -0
  34. package/dist/extensions.d.ts +104 -0
  35. package/dist/extensions.d.ts.map +1 -0
  36. package/dist/extensions.js +284 -0
  37. package/dist/extensions.js.map +1 -0
  38. package/dist/handoffs.d.ts +97 -0
  39. package/dist/handoffs.d.ts.map +1 -0
  40. package/dist/handoffs.js +199 -0
  41. package/dist/handoffs.js.map +1 -0
  42. package/dist/healing.d.ts +56 -0
  43. package/dist/healing.d.ts.map +1 -0
  44. package/dist/healing.js +54 -0
  45. package/dist/healing.js.map +1 -0
  46. package/dist/host.d.ts +61 -0
  47. package/dist/host.d.ts.map +1 -0
  48. package/dist/host.js +369 -0
  49. package/dist/host.js.map +1 -0
  50. package/dist/index.d.ts +26 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +26 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/isolated-worker.d.ts +2 -0
  55. package/dist/isolated-worker.d.ts.map +1 -0
  56. package/dist/isolated-worker.js +155 -0
  57. package/dist/isolated-worker.js.map +1 -0
  58. package/dist/isolated.d.ts +52 -0
  59. package/dist/isolated.d.ts.map +1 -0
  60. package/dist/isolated.js +293 -0
  61. package/dist/isolated.js.map +1 -0
  62. package/dist/native.d.ts +92 -0
  63. package/dist/native.d.ts.map +1 -0
  64. package/dist/native.js +339 -0
  65. package/dist/native.js.map +1 -0
  66. package/dist/orchestration/artifact-port.d.ts +30 -0
  67. package/dist/orchestration/artifact-port.d.ts.map +1 -0
  68. package/dist/orchestration/artifact-port.js +59 -0
  69. package/dist/orchestration/artifact-port.js.map +1 -0
  70. package/dist/orchestration/catalog.d.ts +71 -0
  71. package/dist/orchestration/catalog.d.ts.map +1 -0
  72. package/dist/orchestration/catalog.js +46 -0
  73. package/dist/orchestration/catalog.js.map +1 -0
  74. package/dist/orchestration/compile.d.ts +59 -0
  75. package/dist/orchestration/compile.d.ts.map +1 -0
  76. package/dist/orchestration/compile.js +268 -0
  77. package/dist/orchestration/compile.js.map +1 -0
  78. package/dist/orchestration/dispatch.d.ts +115 -0
  79. package/dist/orchestration/dispatch.d.ts.map +1 -0
  80. package/dist/orchestration/dispatch.js +220 -0
  81. package/dist/orchestration/dispatch.js.map +1 -0
  82. package/dist/orchestration/durable-jobs.d.ts +35 -0
  83. package/dist/orchestration/durable-jobs.d.ts.map +1 -0
  84. package/dist/orchestration/durable-jobs.js +273 -0
  85. package/dist/orchestration/durable-jobs.js.map +1 -0
  86. package/dist/orchestration/effect-key.d.ts +23 -0
  87. package/dist/orchestration/effect-key.d.ts.map +1 -0
  88. package/dist/orchestration/effect-key.js +24 -0
  89. package/dist/orchestration/effect-key.js.map +1 -0
  90. package/dist/orchestration/evidence-store.d.ts +75 -0
  91. package/dist/orchestration/evidence-store.d.ts.map +1 -0
  92. package/dist/orchestration/evidence-store.js +183 -0
  93. package/dist/orchestration/evidence-store.js.map +1 -0
  94. package/dist/orchestration/hash.d.ts +12 -0
  95. package/dist/orchestration/hash.d.ts.map +1 -0
  96. package/dist/orchestration/hash.js +43 -0
  97. package/dist/orchestration/hash.js.map +1 -0
  98. package/dist/orchestration/host.d.ts +121 -0
  99. package/dist/orchestration/host.d.ts.map +1 -0
  100. package/dist/orchestration/host.js +334 -0
  101. package/dist/orchestration/host.js.map +1 -0
  102. package/dist/orchestration/index.d.ts +20 -0
  103. package/dist/orchestration/index.d.ts.map +1 -0
  104. package/dist/orchestration/index.js +19 -0
  105. package/dist/orchestration/index.js.map +1 -0
  106. package/dist/orchestration/policy.d.ts +63 -0
  107. package/dist/orchestration/policy.d.ts.map +1 -0
  108. package/dist/orchestration/policy.js +80 -0
  109. package/dist/orchestration/policy.js.map +1 -0
  110. package/dist/orchestration/recovery.d.ts +87 -0
  111. package/dist/orchestration/recovery.d.ts.map +1 -0
  112. package/dist/orchestration/recovery.js +197 -0
  113. package/dist/orchestration/recovery.js.map +1 -0
  114. package/dist/orchestration/tracer.d.ts +31 -0
  115. package/dist/orchestration/tracer.d.ts.map +1 -0
  116. package/dist/orchestration/tracer.js +141 -0
  117. package/dist/orchestration/tracer.js.map +1 -0
  118. package/dist/plans.d.ts +35 -0
  119. package/dist/plans.d.ts.map +1 -0
  120. package/dist/plans.js +63 -0
  121. package/dist/plans.js.map +1 -0
  122. package/dist/presets.d.ts +10 -0
  123. package/dist/presets.d.ts.map +1 -0
  124. package/dist/presets.js +70 -0
  125. package/dist/presets.js.map +1 -0
  126. package/dist/process.d.ts +6 -0
  127. package/dist/process.d.ts.map +1 -0
  128. package/dist/process.js +42 -0
  129. package/dist/process.js.map +1 -0
  130. package/dist/registry.d.ts +37 -0
  131. package/dist/registry.d.ts.map +1 -0
  132. package/dist/registry.js +51 -0
  133. package/dist/registry.js.map +1 -0
  134. package/dist/replay.d.ts +24 -0
  135. package/dist/replay.d.ts.map +1 -0
  136. package/dist/replay.js +154 -0
  137. package/dist/replay.js.map +1 -0
  138. package/dist/rpc.d.ts +51 -0
  139. package/dist/rpc.d.ts.map +1 -0
  140. package/dist/rpc.js +412 -0
  141. package/dist/rpc.js.map +1 -0
  142. package/dist/session-migration.d.ts +59 -0
  143. package/dist/session-migration.d.ts.map +1 -0
  144. package/dist/session-migration.js +315 -0
  145. package/dist/session-migration.js.map +1 -0
  146. package/dist/sessions.d.ts +74 -0
  147. package/dist/sessions.d.ts.map +1 -0
  148. package/dist/sessions.js +222 -0
  149. package/dist/sessions.js.map +1 -0
  150. package/dist/subagents.d.ts +51 -0
  151. package/dist/subagents.d.ts.map +1 -0
  152. package/dist/subagents.js +141 -0
  153. package/dist/subagents.js.map +1 -0
  154. package/dist/tools.d.ts +13 -0
  155. package/dist/tools.d.ts.map +1 -0
  156. package/dist/tools.js +37 -0
  157. package/dist/tools.js.map +1 -0
  158. package/dist/ui.d.ts +42 -0
  159. package/dist/ui.d.ts.map +1 -0
  160. package/dist/ui.js +93 -0
  161. package/dist/ui.js.map +1 -0
  162. package/dist/verification.d.ts +34 -0
  163. package/dist/verification.d.ts.map +1 -0
  164. package/dist/verification.js +91 -0
  165. package/dist/verification.js.map +1 -0
  166. package/dist/workflows.d.ts +98 -0
  167. package/dist/workflows.d.ts.map +1 -0
  168. package/dist/workflows.js +174 -0
  169. package/dist/workflows.js.map +1 -0
  170. package/package.json +84 -0
  171. package/src/approvals.ts +249 -0
  172. package/src/capabilities.ts +74 -0
  173. package/src/cli.ts +405 -0
  174. package/src/context.ts +81 -0
  175. package/src/deployment-adapters.ts +181 -0
  176. package/src/diff.ts +80 -0
  177. package/src/errors.ts +46 -0
  178. package/src/extensions.ts +402 -0
  179. package/src/handoffs.ts +299 -0
  180. package/src/healing.ts +93 -0
  181. package/src/host.ts +465 -0
  182. package/src/index.ts +165 -0
  183. package/src/isolated-worker.ts +177 -0
  184. package/src/isolated.ts +344 -0
  185. package/src/native.ts +478 -0
  186. package/src/orchestration/artifact-port.ts +75 -0
  187. package/src/orchestration/catalog.ts +102 -0
  188. package/src/orchestration/compile.ts +373 -0
  189. package/src/orchestration/dispatch.ts +383 -0
  190. package/src/orchestration/durable-jobs.ts +374 -0
  191. package/src/orchestration/effect-key.ts +35 -0
  192. package/src/orchestration/evidence-store.ts +233 -0
  193. package/src/orchestration/hash.ts +43 -0
  194. package/src/orchestration/host.ts +445 -0
  195. package/src/orchestration/index.ts +131 -0
  196. package/src/orchestration/policy.ts +116 -0
  197. package/src/orchestration/recovery.ts +271 -0
  198. package/src/orchestration/tracer.ts +177 -0
  199. package/src/plans.ts +105 -0
  200. package/src/presets.ts +90 -0
  201. package/src/process.ts +47 -0
  202. package/src/registry.ts +80 -0
  203. package/src/replay.ts +190 -0
  204. package/src/rpc.ts +598 -0
  205. package/src/session-migration.ts +401 -0
  206. package/src/sessions.ts +303 -0
  207. package/src/subagents.ts +210 -0
  208. package/src/tools.ts +52 -0
  209. package/src/ui.ts +137 -0
  210. package/src/verification.ts +133 -0
  211. package/src/workflows.ts +279 -0
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Small, backend-neutral capability manifest.
3
+ *
4
+ * The manifest is deliberately descriptive: an operator still decides which capabilities are
5
+ * trusted, and implementations remain free to enforce them with a sandbox, OS policy, or approval
6
+ * broker. Keeping this seam in the optional agent package avoids putting process/filesystem policy
7
+ * into Nifra's framework runtime.
8
+ */
9
+ export const AGENT_CAPABILITY_MANIFEST_VERSION = 1 as const
10
+
11
+ export type AgentCapability =
12
+ | "filesystem.read"
13
+ | "filesystem.write"
14
+ | "process.exec"
15
+ | "network.request"
16
+ | "credentials.read"
17
+
18
+ export interface AgentCapabilityManifest {
19
+ readonly version: typeof AGENT_CAPABILITY_MANIFEST_VERSION
20
+ readonly requested: readonly AgentCapability[]
21
+ readonly trusted: readonly AgentCapability[]
22
+ readonly reason?: string
23
+ }
24
+
25
+ export function createCapabilityManifest(
26
+ requested: readonly AgentCapability[],
27
+ trusted: readonly AgentCapability[] = [],
28
+ reason?: string,
29
+ ): AgentCapabilityManifest {
30
+ const unique = (values: readonly AgentCapability[]): readonly AgentCapability[] =>
31
+ Object.freeze([...new Set(values)])
32
+ return Object.freeze({
33
+ version: AGENT_CAPABILITY_MANIFEST_VERSION,
34
+ requested: unique(requested),
35
+ trusted: unique(trusted),
36
+ ...(reason === undefined ? {} : { reason: reason.slice(0, 512) }),
37
+ })
38
+ }
39
+
40
+ export function deniedCapabilities(manifest: AgentCapabilityManifest): readonly AgentCapability[] {
41
+ const trusted = new Set(manifest.trusted)
42
+ return Object.freeze(manifest.requested.filter((capability) => !trusted.has(capability)))
43
+ }
44
+
45
+ export function parseCapabilityManifest(value: unknown): AgentCapabilityManifest {
46
+ if (value === null || typeof value !== "object" || Array.isArray(value))
47
+ throw new TypeError("capability manifest must be an object")
48
+ const record = value as Record<string, unknown>
49
+ if (record.version !== AGENT_CAPABILITY_MANIFEST_VERSION)
50
+ throw new TypeError("unsupported capability manifest version")
51
+ const requested = parseCapabilities(record.requested, "requested")
52
+ const trusted = parseCapabilities(record.trusted, "trusted")
53
+ if (record.reason !== undefined && typeof record.reason !== "string")
54
+ throw new TypeError("capability manifest reason must be a string")
55
+ return createCapabilityManifest(
56
+ requested,
57
+ trusted,
58
+ typeof record.reason === "string" ? record.reason : undefined,
59
+ )
60
+ }
61
+
62
+ function parseCapabilities(value: unknown, name: string): readonly AgentCapability[] {
63
+ if (!Array.isArray(value)) throw new TypeError(`capability manifest ${name} must be an array`)
64
+ const allowed = new Set<AgentCapability>([
65
+ "filesystem.read",
66
+ "filesystem.write",
67
+ "process.exec",
68
+ "network.request",
69
+ "credentials.read",
70
+ ])
71
+ if (value.some((item) => typeof item !== "string" || !allowed.has(item as AgentCapability)))
72
+ throw new TypeError(`capability manifest ${name} contains an unknown capability`)
73
+ return value as AgentCapability[]
74
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,405 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { join, resolve } from "node:path"
4
+ import { createInterface } from "node:readline"
5
+ import type { AgentEvent } from "@nifrajs/agent-protocol"
6
+ import {
7
+ CodingAgentHost,
8
+ CodingAgentRpcServer,
9
+ discoverExtensions,
10
+ ExtensionHost,
11
+ FileSessionStore,
12
+ migrateLegacySession,
13
+ NIFRA_AGENT_INSTRUCTIONS,
14
+ PiBackend,
15
+ ReplayBackend,
16
+ readReplayEvents,
17
+ runNifraContext,
18
+ validateExtensionModule,
19
+ } from "./index.ts"
20
+ import { runNifraVerification } from "./verification.ts"
21
+
22
+ const HELP = `nifra-agent - local coding agent host
23
+
24
+ Usage:
25
+ nifra-agent [--backend pi|replay] [--cwd <dir>] [--once <prompt>]
26
+ [--json] [--no-session] [--session-dir <dir>] [--session-id <id>]
27
+ [--verify-after-turn check,assure] [--max-repair-attempts <n>]
28
+ [--pi <command>] [--replay <file>]
29
+ nifra-agent --migrate-session <id> --migrate-from <dir> --migrate-to <dir> [--json]
30
+ nifra-agent --rpc [--cwd <dir>] [--host 127.0.0.1] [--port 0] [--expose-error-stacks]
31
+
32
+ Commands in interactive mode:
33
+ /reload reload Pi or Nifra extensions
34
+ /compact compact the bounded prompt window
35
+ /checkpoint persist a recoverable session checkpoint
36
+ /fork [ID] fork the current session history
37
+ /history [N] show bounded persisted session history
38
+ /check run nifra check --json
39
+ /assure run nifra assure --json
40
+ /context inspect the current Nifra project context
41
+ /approvals list pending approvals
42
+ /approve ID approve a pending action
43
+ /deny ID deny a pending action
44
+ /quit close the session
45
+ `
46
+
47
+ export interface CliOptions {
48
+ readonly backend: string
49
+ readonly cwd: string
50
+ readonly once?: string
51
+ readonly json: boolean
52
+ readonly noSession: boolean
53
+ readonly piCommand: string
54
+ readonly replayFile?: string
55
+ readonly sessionDir?: string
56
+ readonly rpc: boolean
57
+ readonly host: string
58
+ readonly port: number
59
+ readonly authToken?: string
60
+ readonly exposeErrorStacks: boolean
61
+ readonly sessionId?: string
62
+ readonly migrateSession?: string
63
+ readonly migrationSource?: string
64
+ readonly migrationTarget?: string
65
+ readonly verifyAfterTurn: readonly ("check" | "assure" | "test")[]
66
+ readonly maxRepairAttempts: number
67
+ }
68
+
69
+ export function parseArgs(args: readonly string[]): CliOptions {
70
+ let backend = "pi"
71
+ let cwd = process.cwd()
72
+ let once: string | undefined
73
+ let json = false
74
+ let noSession = false
75
+ let piCommand = "pi"
76
+ let replayFile: string | undefined
77
+ let sessionDir: string | undefined
78
+ let rpc = false
79
+ let host = "127.0.0.1"
80
+ let port = 0
81
+ let authToken: string | undefined
82
+ let exposeErrorStacks = false
83
+ let sessionId: string | undefined
84
+ let migrateSession: string | undefined
85
+ let migrationSource: string | undefined
86
+ let migrationTarget: string | undefined
87
+ let verifyAfterTurn: readonly ("check" | "assure" | "test")[] = []
88
+ let maxRepairAttempts = 2
89
+ for (let index = 0; index < args.length; index++) {
90
+ const arg = args[index]
91
+ if (arg === "--backend") backend = args[++index] ?? backend
92
+ else if (arg === "--cwd") cwd = resolve(args[++index] ?? cwd)
93
+ else if (arg === "--once" || arg === "--message") once = args[++index]
94
+ else if (arg === "--json") json = true
95
+ else if (arg === "--no-session") noSession = true
96
+ else if (arg === "--session-dir") sessionDir = resolve(args[++index] ?? "")
97
+ else if (arg === "--session-id") sessionId = args[++index]
98
+ else if (arg === "--migrate-session") migrateSession = args[++index]
99
+ else if (arg === "--migrate-from") migrationSource = resolve(args[++index] ?? "")
100
+ else if (arg === "--migrate-to") migrationTarget = resolve(args[++index] ?? "")
101
+ else if (arg === "--pi") piCommand = args[++index] ?? piCommand
102
+ else if (arg === "--replay") replayFile = resolve(args[++index] ?? "")
103
+ else if (arg === "--rpc") rpc = true
104
+ else if (arg === "--host") host = args[++index] ?? host
105
+ else if (arg === "--port") {
106
+ port = Number(args[++index] ?? "0")
107
+ if (!Number.isSafeInteger(port) || port < 0 || port > 65_535)
108
+ throw new Error("--port must be an integer between 0 and 65535")
109
+ } else if (arg === "--token") authToken = args[++index]
110
+ else if (arg === "--expose-error-stacks") exposeErrorStacks = true
111
+ else if (arg === "--verify-after-turn")
112
+ verifyAfterTurn = parseVerificationNames(args[++index] ?? "")
113
+ else if (arg === "--max-repair-attempts") {
114
+ maxRepairAttempts = Number(args[++index] ?? "")
115
+ if (
116
+ !Number.isSafeInteger(maxRepairAttempts) ||
117
+ maxRepairAttempts < 0 ||
118
+ maxRepairAttempts > 8
119
+ )
120
+ throw new Error("--max-repair-attempts must be an integer between 0 and 8")
121
+ } else if (arg === "--help" || arg === "-h") {
122
+ console.log(HELP)
123
+ process.exit(0)
124
+ } else throw new Error(`unknown argument: ${arg}`)
125
+ }
126
+ if (backend !== "pi" && backend !== "replay") throw new Error(`unsupported backend: ${backend}`)
127
+ if (backend === "replay" && replayFile === undefined)
128
+ throw new Error("--backend replay requires --replay <file>")
129
+ if (
130
+ migrateSession === undefined &&
131
+ (migrationSource !== undefined || migrationTarget !== undefined)
132
+ )
133
+ throw new Error("--migrate-from and --migrate-to require --migrate-session")
134
+ if (
135
+ migrateSession !== undefined &&
136
+ (migrationSource === undefined || migrationTarget === undefined)
137
+ )
138
+ throw new Error("--migrate-session requires --migrate-from and --migrate-to")
139
+ return {
140
+ backend,
141
+ cwd,
142
+ ...(once === undefined ? {} : { once }),
143
+ json,
144
+ noSession,
145
+ piCommand,
146
+ ...(replayFile === undefined ? {} : { replayFile }),
147
+ ...(sessionDir === undefined ? {} : { sessionDir }),
148
+ rpc,
149
+ host,
150
+ port,
151
+ ...(authToken === undefined ? {} : { authToken }),
152
+ exposeErrorStacks,
153
+ ...(sessionId === undefined ? {} : { sessionId }),
154
+ ...(migrateSession === undefined ? {} : { migrateSession }),
155
+ ...(migrationSource === undefined ? {} : { migrationSource }),
156
+ ...(migrationTarget === undefined ? {} : { migrationTarget }),
157
+ verifyAfterTurn,
158
+ maxRepairAttempts,
159
+ }
160
+ }
161
+
162
+ function parseVerificationNames(value: string): readonly ("check" | "assure" | "test")[] {
163
+ const names = value
164
+ .split(",")
165
+ .map((name) => name.trim())
166
+ .filter(Boolean)
167
+ if (
168
+ names.length === 0 ||
169
+ names.some((name) => name !== "check" && name !== "assure" && name !== "test")
170
+ )
171
+ throw new Error("--verify-after-turn must contain only check, assure, or test")
172
+ return Object.freeze([...new Set(names)] as ("check" | "assure" | "test")[])
173
+ }
174
+
175
+ function printEvent(event: AgentEvent, json: boolean): void {
176
+ if (json) {
177
+ console.log(JSON.stringify(event))
178
+ return
179
+ }
180
+ if (event.type === "assistant.delta") process.stdout.write(String(event.text ?? ""))
181
+ else if (event.type === "assistant.message")
182
+ process.stdout.write(`\n${String(event.text ?? "")}\n`)
183
+ else if (event.type === "tool.started")
184
+ console.error(`\n[tool] ${String(event.name ?? "unknown")}`)
185
+ else if (event.type === "tool.completed")
186
+ console.error(`[tool ${event.ok === true ? "ok" : "failed"}]`)
187
+ else if (event.type === "memory.compacted")
188
+ console.error(`[memory] compacted ${String(event.before)} -> ${String(event.after)}`)
189
+ else if (event.type === "extension.reloaded") console.error(`[reload] ${String(event.revision)}`)
190
+ else if (event.type === "session.failed") console.error(`[agent] ${JSON.stringify(event.error)}`)
191
+ }
192
+
193
+ async function main(): Promise<void> {
194
+ const options = parseArgs(Bun.argv.slice(2))
195
+ if (options.migrateSession !== undefined) {
196
+ const report = await migrateLegacySession({
197
+ sessionId: options.migrateSession,
198
+ sourceRoot: options.migrationSource!,
199
+ targetRoot: options.migrationTarget!,
200
+ })
201
+ if (options.json) console.log(JSON.stringify({ type: "session.migrated", report }))
202
+ else
203
+ console.log(
204
+ `[session] migrated ${report.records} evidence records (${report.firstSeq ?? "empty"}..${report.lastSeq ?? "empty"}) digest ${report.digest}`,
205
+ )
206
+ return
207
+ }
208
+ const backend =
209
+ options.backend === "replay"
210
+ ? new ReplayBackend({ events: await readReplayEvents(options.replayFile!) })
211
+ : new PiBackend({
212
+ command: options.piCommand,
213
+ noSession: options.noSession,
214
+ appendSystemPrompt: NIFRA_AGENT_INSTRUCTIONS,
215
+ enableNifraTools: true,
216
+ ...(options.sessionDir === undefined ? {} : { sessionDir: options.sessionDir }),
217
+ })
218
+ const extensionRoots = await discoverExtensions(options.cwd)
219
+ const extensions = new ExtensionHost({
220
+ cwd: options.cwd,
221
+ roots: extensionRoots,
222
+ validate: validateExtensionModule,
223
+ })
224
+
225
+ if (options.rpc) {
226
+ const rpc = new CodingAgentRpcServer({
227
+ backend,
228
+ cwd: options.cwd,
229
+ hostname: options.host,
230
+ port: options.port,
231
+ ...(options.authToken === undefined ? {} : { authToken: options.authToken }),
232
+ exposeErrorStacks: options.exposeErrorStacks,
233
+ ...(options.sessionDir === undefined
234
+ ? {}
235
+ : { sessionStore: new FileSessionStore({ root: join(options.sessionDir, "events") }) }),
236
+ extensions,
237
+ maxRepairAttempts: options.maxRepairAttempts,
238
+ verifyAfterTurn: options.verifyAfterTurn,
239
+ })
240
+ const handle = await rpc.start()
241
+ const ready = { type: "rpc.ready", url: handle.url, token: handle.token, protocol: 1 }
242
+ if (options.json) console.log(JSON.stringify(ready))
243
+ else console.error(`nifra-agent RPC listening at ${handle.url} (token: ${handle.token})`)
244
+ const stopRpc = async (code: number): Promise<void> => {
245
+ await rpc.stop()
246
+ process.exit(code)
247
+ }
248
+ process.once("SIGINT", () => void stopRpc(130))
249
+ process.once("SIGTERM", () => void stopRpc(143))
250
+ await new Promise<void>(() => {})
251
+ return
252
+ }
253
+
254
+ const hostOptions =
255
+ options.sessionDir === undefined
256
+ ? {
257
+ backend,
258
+ extensions,
259
+ maxRepairAttempts: options.maxRepairAttempts,
260
+ verifyAfterTurn: options.verifyAfterTurn,
261
+ }
262
+ : {
263
+ backend,
264
+ sessionStore: new FileSessionStore({ root: join(options.sessionDir, "events") }),
265
+ extensions,
266
+ maxRepairAttempts: options.maxRepairAttempts,
267
+ verifyAfterTurn: options.verifyAfterTurn,
268
+ }
269
+ const host = new CodingAgentHost(hostOptions)
270
+ await host.start({
271
+ cwd: options.cwd,
272
+ backend: options.backend,
273
+ ...(options.sessionId === undefined ? {} : { sessionId: options.sessionId }),
274
+ })
275
+ const stop = async (): Promise<void> => {
276
+ await host.stop("process exit")
277
+ }
278
+ process.once("SIGINT", () => void stop().finally(() => process.exit(130)))
279
+ process.once("SIGTERM", () => void stop().finally(() => process.exit(143)))
280
+
281
+ const send = async (message: string): Promise<void> => {
282
+ for await (const event of host.prompt(message)) printEvent(event, options.json)
283
+ }
284
+
285
+ if (options.once !== undefined) {
286
+ await send(options.once)
287
+ await stop()
288
+ return
289
+ }
290
+
291
+ if (!options.json) {
292
+ console.log("nifra-agent connected to Pi. Type a prompt, /reload, /check, or /quit.")
293
+ }
294
+ const readline = createInterface({
295
+ input: process.stdin,
296
+ output: options.json ? undefined : process.stdout,
297
+ })
298
+ try {
299
+ for await (const line of readline) {
300
+ const message = line.trim()
301
+ if (message.length === 0) continue
302
+ if (message === "/quit" || message === "/exit") break
303
+ if (message === "/reload") {
304
+ const result = await host.reload()
305
+ if (options.json) console.log(JSON.stringify({ type: "extension.reloaded", ...result }))
306
+ else console.error(`[reload] ${result.rolledBack ? "rolled back" : result.revision}`)
307
+ continue
308
+ }
309
+ if (message === "/compact") {
310
+ const result = await host.compact("manual")
311
+ if (options.json) console.log(JSON.stringify({ type: "memory.compacted", ...result }))
312
+ else console.error(`[memory] compacted ${result.before} -> ${result.after}`)
313
+ continue
314
+ }
315
+ if (message === "/checkpoint") {
316
+ await host.checkpoint()
317
+ if (options.json) console.log(JSON.stringify({ type: "session.checkpoint", ok: true }))
318
+ else console.error("[session] checkpoint saved")
319
+ continue
320
+ }
321
+ const forkCommand = /^\/fork(?:\s+([^\s]+))?$/.exec(message)
322
+ if (forkCommand !== null) {
323
+ const result = await host.fork(forkCommand[1])
324
+ if (options.json) console.log(JSON.stringify({ type: "session.forked", ...result }))
325
+ else console.log(`[session] forked ${result.sessionId}`)
326
+ continue
327
+ }
328
+ const historyCommand = /^\/history(?:\s+(\d+))?$/.exec(message)
329
+ if (historyCommand !== null) {
330
+ const result = await host.history(
331
+ historyCommand[1] === undefined ? 20 : Number(historyCommand[1]),
332
+ )
333
+ if (options.json) console.log(JSON.stringify({ type: "session.history", entries: result }))
334
+ else for (const entry of result) console.log(`${entry.seq} · ${entry.type}`)
335
+ continue
336
+ }
337
+ if (message === "/check") {
338
+ const result = await runNifraVerification("check", { cwd: options.cwd })
339
+ if (options.json) console.log(JSON.stringify({ type: "verification.completed", ...result }))
340
+ else
341
+ console.error(
342
+ `[check] ${result.ok ? "passed" : "failed"}${result.status === null ? "" : ` (exit ${result.status})`}`,
343
+ )
344
+ continue
345
+ }
346
+ if (message === "/assure") {
347
+ const result = await runNifraVerification("assure", { cwd: options.cwd })
348
+ if (options.json) console.log(JSON.stringify({ type: "verification.completed", ...result }))
349
+ else
350
+ console.error(
351
+ `[assure] ${result.ok ? "passed" : "failed"}${result.status === null ? "" : ` (exit ${result.status})`}`,
352
+ )
353
+ continue
354
+ }
355
+ if (message === "/context") {
356
+ const result = await runNifraContext({ cwd: options.cwd })
357
+ if (options.json) console.log(JSON.stringify({ type: "context.completed", ...result }))
358
+ else console.log(result.output ?? JSON.stringify(result.report ?? result))
359
+ continue
360
+ }
361
+ if (message === "/approvals") {
362
+ const result = host.pendingApprovals
363
+ if (options.json) console.log(JSON.stringify({ type: "approval.list", pending: result }))
364
+ else if (result.length === 0) console.log("No pending approvals")
365
+ else
366
+ for (const approval of result)
367
+ console.log(`${approval.id} · ${approval.capability} · ${approval.action}`)
368
+ continue
369
+ }
370
+ const approvalCommand = /^(\/approve|\/deny)\s+([^\s]+)$/.exec(message)
371
+ if (approvalCommand !== null) {
372
+ const result = await host.resolveApproval(
373
+ approvalCommand[2]!,
374
+ approvalCommand[1] === "/approve",
375
+ "CLI decision",
376
+ )
377
+ if (options.json)
378
+ console.log(
379
+ JSON.stringify({
380
+ type: "approval.resolved",
381
+ ...(result ?? { error: "approval not found" }),
382
+ }),
383
+ )
384
+ else
385
+ console.log(
386
+ result === undefined
387
+ ? "Approval not found"
388
+ : `${result.approved ? "Approved" : "Denied"} ${result.approvalId}`,
389
+ )
390
+ continue
391
+ }
392
+ await send(message)
393
+ }
394
+ } finally {
395
+ readline.close()
396
+ await stop()
397
+ }
398
+ }
399
+
400
+ if (import.meta.main) {
401
+ main().catch((error) => {
402
+ console.error(error instanceof Error ? error.message : String(error))
403
+ process.exitCode = 1
404
+ })
405
+ }
package/src/context.ts ADDED
@@ -0,0 +1,81 @@
1
+ import { readBoundedText } from "./process.ts"
2
+
3
+ export interface NifraContextOptions {
4
+ readonly cwd: string
5
+ readonly command?: string
6
+ readonly timeoutMs?: number
7
+ readonly maxOutputBytes?: number
8
+ readonly env?: Readonly<Record<string, string | undefined>>
9
+ }
10
+
11
+ export interface NifraContextResult {
12
+ readonly ok: boolean
13
+ readonly status: number | null
14
+ readonly output?: string
15
+ readonly report?: unknown
16
+ readonly error?: string
17
+ }
18
+
19
+ /** Project discovery kept outside the framework runtime; it is only spawned when an agent asks for it. */
20
+ export async function runNifraContext(options: NifraContextOptions): Promise<NifraContextResult> {
21
+ const proc = Bun.spawn([options.command ?? "nifra", "context"], {
22
+ cwd: options.cwd,
23
+ stdin: "ignore",
24
+ stdout: "pipe",
25
+ stderr: "pipe",
26
+ env: safeEnv(options.env),
27
+ })
28
+ const timeoutMs = options.timeoutMs ?? 30_000
29
+ let timedOut = false
30
+ const timer = setTimeout(() => {
31
+ timedOut = true
32
+ proc.kill()
33
+ }, timeoutMs)
34
+ try {
35
+ const [stdout, stderr, status] = await Promise.all([
36
+ readBoundedText(proc.stdout, options.maxOutputBytes ?? 1_048_576),
37
+ readBoundedText(proc.stderr, options.maxOutputBytes ?? 1_048_576),
38
+ proc.exited,
39
+ ])
40
+ let report: unknown
41
+ try {
42
+ report = JSON.parse(stdout.text)
43
+ } catch {
44
+ report = undefined
45
+ }
46
+ return {
47
+ ok: !timedOut && status === 0,
48
+ status,
49
+ ...(stdout.text || stderr.text ? { output: stdout.text || stderr.text } : {}),
50
+ ...(report === undefined ? {} : { report }),
51
+ ...(timedOut ? { error: `nifra context timed out after ${timeoutMs}ms` } : {}),
52
+ }
53
+ } catch (error) {
54
+ return {
55
+ ok: false,
56
+ status: null,
57
+ error: error instanceof Error ? error.message : String(error),
58
+ }
59
+ } finally {
60
+ clearTimeout(timer)
61
+ }
62
+ }
63
+
64
+ function safeEnv(
65
+ overrides: Readonly<Record<string, string | undefined>> | undefined,
66
+ ): Record<string, string> {
67
+ const result: Record<string, string> = {}
68
+ for (const name of new Set([
69
+ "PATH",
70
+ "HOME",
71
+ "LANG",
72
+ "LC_ALL",
73
+ "TERM",
74
+ "CI",
75
+ ...Object.keys(overrides ?? {}),
76
+ ])) {
77
+ const value = overrides?.[name] ?? process.env[name]
78
+ if (value !== undefined) result[name] = value
79
+ }
80
+ return result
81
+ }