@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.
- package/EXTENSIONS.md +25 -0
- package/PERFORMANCE.md +19 -0
- package/README.md +69 -0
- package/REGISTRY.md +15 -0
- package/SECURITY.md +32 -0
- package/dist/approvals.d.ts +70 -0
- package/dist/approvals.d.ts.map +1 -0
- package/dist/approvals.js +174 -0
- package/dist/approvals.js.map +1 -0
- package/dist/capabilities.d.ts +20 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +49 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/cli.d.ts +24 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +380 -0
- package/dist/cli.js.map +1 -0
- package/dist/context.d.ts +17 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +66 -0
- package/dist/context.js.map +1 -0
- package/dist/deployment-adapters.d.ts +40 -0
- package/dist/deployment-adapters.d.ts.map +1 -0
- package/dist/deployment-adapters.js +147 -0
- package/dist/deployment-adapters.js.map +1 -0
- package/dist/diff.d.ts +17 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +61 -0
- package/dist/diff.js.map +1 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +35 -0
- package/dist/errors.js.map +1 -0
- package/dist/extensions.d.ts +104 -0
- package/dist/extensions.d.ts.map +1 -0
- package/dist/extensions.js +284 -0
- package/dist/extensions.js.map +1 -0
- package/dist/handoffs.d.ts +97 -0
- package/dist/handoffs.d.ts.map +1 -0
- package/dist/handoffs.js +199 -0
- package/dist/handoffs.js.map +1 -0
- package/dist/healing.d.ts +56 -0
- package/dist/healing.d.ts.map +1 -0
- package/dist/healing.js +54 -0
- package/dist/healing.js.map +1 -0
- package/dist/host.d.ts +61 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +369 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/isolated-worker.d.ts +2 -0
- package/dist/isolated-worker.d.ts.map +1 -0
- package/dist/isolated-worker.js +155 -0
- package/dist/isolated-worker.js.map +1 -0
- package/dist/isolated.d.ts +52 -0
- package/dist/isolated.d.ts.map +1 -0
- package/dist/isolated.js +293 -0
- package/dist/isolated.js.map +1 -0
- package/dist/native.d.ts +92 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +339 -0
- package/dist/native.js.map +1 -0
- package/dist/orchestration/artifact-port.d.ts +30 -0
- package/dist/orchestration/artifact-port.d.ts.map +1 -0
- package/dist/orchestration/artifact-port.js +59 -0
- package/dist/orchestration/artifact-port.js.map +1 -0
- package/dist/orchestration/catalog.d.ts +71 -0
- package/dist/orchestration/catalog.d.ts.map +1 -0
- package/dist/orchestration/catalog.js +46 -0
- package/dist/orchestration/catalog.js.map +1 -0
- package/dist/orchestration/compile.d.ts +59 -0
- package/dist/orchestration/compile.d.ts.map +1 -0
- package/dist/orchestration/compile.js +268 -0
- package/dist/orchestration/compile.js.map +1 -0
- package/dist/orchestration/dispatch.d.ts +115 -0
- package/dist/orchestration/dispatch.d.ts.map +1 -0
- package/dist/orchestration/dispatch.js +220 -0
- package/dist/orchestration/dispatch.js.map +1 -0
- package/dist/orchestration/durable-jobs.d.ts +35 -0
- package/dist/orchestration/durable-jobs.d.ts.map +1 -0
- package/dist/orchestration/durable-jobs.js +273 -0
- package/dist/orchestration/durable-jobs.js.map +1 -0
- package/dist/orchestration/effect-key.d.ts +23 -0
- package/dist/orchestration/effect-key.d.ts.map +1 -0
- package/dist/orchestration/effect-key.js +24 -0
- package/dist/orchestration/effect-key.js.map +1 -0
- package/dist/orchestration/evidence-store.d.ts +75 -0
- package/dist/orchestration/evidence-store.d.ts.map +1 -0
- package/dist/orchestration/evidence-store.js +183 -0
- package/dist/orchestration/evidence-store.js.map +1 -0
- package/dist/orchestration/hash.d.ts +12 -0
- package/dist/orchestration/hash.d.ts.map +1 -0
- package/dist/orchestration/hash.js +43 -0
- package/dist/orchestration/hash.js.map +1 -0
- package/dist/orchestration/host.d.ts +121 -0
- package/dist/orchestration/host.d.ts.map +1 -0
- package/dist/orchestration/host.js +334 -0
- package/dist/orchestration/host.js.map +1 -0
- package/dist/orchestration/index.d.ts +20 -0
- package/dist/orchestration/index.d.ts.map +1 -0
- package/dist/orchestration/index.js +19 -0
- package/dist/orchestration/index.js.map +1 -0
- package/dist/orchestration/policy.d.ts +63 -0
- package/dist/orchestration/policy.d.ts.map +1 -0
- package/dist/orchestration/policy.js +80 -0
- package/dist/orchestration/policy.js.map +1 -0
- package/dist/orchestration/recovery.d.ts +87 -0
- package/dist/orchestration/recovery.d.ts.map +1 -0
- package/dist/orchestration/recovery.js +197 -0
- package/dist/orchestration/recovery.js.map +1 -0
- package/dist/orchestration/tracer.d.ts +31 -0
- package/dist/orchestration/tracer.d.ts.map +1 -0
- package/dist/orchestration/tracer.js +141 -0
- package/dist/orchestration/tracer.js.map +1 -0
- package/dist/plans.d.ts +35 -0
- package/dist/plans.d.ts.map +1 -0
- package/dist/plans.js +63 -0
- package/dist/plans.js.map +1 -0
- package/dist/presets.d.ts +10 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +70 -0
- package/dist/presets.js.map +1 -0
- package/dist/process.d.ts +6 -0
- package/dist/process.d.ts.map +1 -0
- package/dist/process.js +42 -0
- package/dist/process.js.map +1 -0
- package/dist/registry.d.ts +37 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +51 -0
- package/dist/registry.js.map +1 -0
- package/dist/replay.d.ts +24 -0
- package/dist/replay.d.ts.map +1 -0
- package/dist/replay.js +154 -0
- package/dist/replay.js.map +1 -0
- package/dist/rpc.d.ts +51 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +412 -0
- package/dist/rpc.js.map +1 -0
- package/dist/session-migration.d.ts +59 -0
- package/dist/session-migration.d.ts.map +1 -0
- package/dist/session-migration.js +315 -0
- package/dist/session-migration.js.map +1 -0
- package/dist/sessions.d.ts +74 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +222 -0
- package/dist/sessions.js.map +1 -0
- package/dist/subagents.d.ts +51 -0
- package/dist/subagents.d.ts.map +1 -0
- package/dist/subagents.js +141 -0
- package/dist/subagents.js.map +1 -0
- package/dist/tools.d.ts +13 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +37 -0
- package/dist/tools.js.map +1 -0
- package/dist/ui.d.ts +42 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +93 -0
- package/dist/ui.js.map +1 -0
- package/dist/verification.d.ts +34 -0
- package/dist/verification.d.ts.map +1 -0
- package/dist/verification.js +91 -0
- package/dist/verification.js.map +1 -0
- package/dist/workflows.d.ts +98 -0
- package/dist/workflows.d.ts.map +1 -0
- package/dist/workflows.js +174 -0
- package/dist/workflows.js.map +1 -0
- package/package.json +84 -0
- package/src/approvals.ts +249 -0
- package/src/capabilities.ts +74 -0
- package/src/cli.ts +405 -0
- package/src/context.ts +81 -0
- package/src/deployment-adapters.ts +181 -0
- package/src/diff.ts +80 -0
- package/src/errors.ts +46 -0
- package/src/extensions.ts +402 -0
- package/src/handoffs.ts +299 -0
- package/src/healing.ts +93 -0
- package/src/host.ts +465 -0
- package/src/index.ts +165 -0
- package/src/isolated-worker.ts +177 -0
- package/src/isolated.ts +344 -0
- package/src/native.ts +478 -0
- package/src/orchestration/artifact-port.ts +75 -0
- package/src/orchestration/catalog.ts +102 -0
- package/src/orchestration/compile.ts +373 -0
- package/src/orchestration/dispatch.ts +383 -0
- package/src/orchestration/durable-jobs.ts +374 -0
- package/src/orchestration/effect-key.ts +35 -0
- package/src/orchestration/evidence-store.ts +233 -0
- package/src/orchestration/hash.ts +43 -0
- package/src/orchestration/host.ts +445 -0
- package/src/orchestration/index.ts +131 -0
- package/src/orchestration/policy.ts +116 -0
- package/src/orchestration/recovery.ts +271 -0
- package/src/orchestration/tracer.ts +177 -0
- package/src/plans.ts +105 -0
- package/src/presets.ts +90 -0
- package/src/process.ts +47 -0
- package/src/registry.ts +80 -0
- package/src/replay.ts +190 -0
- package/src/rpc.ts +598 -0
- package/src/session-migration.ts +401 -0
- package/src/sessions.ts +303 -0
- package/src/subagents.ts +210 -0
- package/src/tools.ts +52 -0
- package/src/ui.ts +137 -0
- package/src/verification.ts +133 -0
- 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
|
+
}
|