@pi-in-go/pigpen-acp 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.
- package/CREDITS.md +23 -0
- package/LICENSE +22 -0
- package/README.md +110 -0
- package/extensions/acp/cmd/pig-acp/e2e_llm_test.go +168 -0
- package/extensions/acp/cmd/pig-acp/e2e_test.go +541 -0
- package/extensions/acp/cmd/pig-acp/go.mod +6 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent.go +784 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_builtin_test.go +536 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_commands_test.go +199 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_config_test.go +530 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_helpers_test.go +215 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_load_test.go +333 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_prompt.go +289 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_session_test.go +429 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/auth.go +61 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/bench_test.go +47 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/helpers_test.go +421 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/main_test.go +28 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/mutation_extra_test.go +142 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/paths.go +77 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/pisessions.go +317 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/protocol.go +188 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/server.go +218 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/server_test.go +386 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/session.go +1068 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/session_diff_test.go +183 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/session_events_test.go +443 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/session_extra_test.go +547 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/session_usage_test.go +198 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/settings.go +79 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/slash.go +278 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/slash_test.go +128 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/startup.go +156 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/store.go +118 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/store_paths_test.go +466 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/translate.go +352 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/translate_test.go +105 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/twin_test.go +28 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/types.go +202 -0
- package/extensions/acp/cmd/pig-acp/internal/jsonrpc/bench_test.go +24 -0
- package/extensions/acp/cmd/pig-acp/internal/jsonrpc/jsonrpc.go +302 -0
- package/extensions/acp/cmd/pig-acp/internal/jsonrpc/jsonrpc_test.go +320 -0
- package/extensions/acp/cmd/pig-acp/internal/jsonrpc/twin_test.go +28 -0
- package/extensions/acp/cmd/pig-acp/internal/pirpc/kill_unix.go +19 -0
- package/extensions/acp/cmd/pig-acp/internal/pirpc/kill_windows.go +11 -0
- package/extensions/acp/cmd/pig-acp/internal/pirpc/process.go +530 -0
- package/extensions/acp/cmd/pig-acp/internal/pirpc/process_test.go +355 -0
- package/extensions/acp/cmd/pig-acp/internal/pirpc/twin_test.go +28 -0
- package/extensions/acp/cmd/pig-acp/main.go +177 -0
- package/extensions/acp/cmd/pig-acp/main_test.go +213 -0
- package/extensions/acp/cmd/pig-acp/sigpipe_unix.go +10 -0
- package/extensions/acp/cmd/pig-acp/sigpipe_windows.go +5 -0
- package/extensions/acp/cmd/pig-acp/testdata/ask/extension.go +28 -0
- package/extensions/acp/cmd/pig-acp/testdata/ask/go.mod +7 -0
- package/extensions/acp/cmd/pig-acp/testdata/ask/go.sum +2 -0
- package/extensions/acp/cmd/pig-acp/testdata/gate/extension.go +37 -0
- package/extensions/acp/cmd/pig-acp/testdata/gate/go.mod +7 -0
- package/extensions/acp/cmd/pig-acp/testdata/gate/go.sum +2 -0
- package/extensions/acp/cmd/pig-acp/twin_test.go +28 -0
- package/extensions/acp/extension.go +63 -0
- package/extensions/acp/extension_test.go +111 -0
- package/extensions/acp/fakehost_test.go +548 -0
- package/extensions/acp/go.mod +6 -0
- package/extensions/acp/go.sum +2 -0
- package/package.json +41 -0
- package/port/PORT.md +147 -0
- package/port/interop/package-lock.json +89 -0
- package/port/interop/package.json +13 -0
- package/port/interop/run.mjs +229 -0
- package/port/mutate.mjs +110 -0
- package/port/mutation-results.txt +107 -0
- package/port/mutations.json +748 -0
- package/port/oracle/pi-acp/LICENSE +21 -0
- package/port/oracle/pi-acp/README.md +209 -0
- package/port/oracle/pi-acp/package.json +65 -0
- package/port/oracle/pi-acp/src/acp/agent.ts +1661 -0
- package/port/oracle/pi-acp/src/acp/auth-required.ts +37 -0
- package/port/oracle/pi-acp/src/acp/auth.ts +59 -0
- package/port/oracle/pi-acp/src/acp/paths.ts +15 -0
- package/port/oracle/pi-acp/src/acp/pi-commands.ts +59 -0
- package/port/oracle/pi-acp/src/acp/pi-sessions.ts +333 -0
- package/port/oracle/pi-acp/src/acp/pi-settings.ts +75 -0
- package/port/oracle/pi-acp/src/acp/session-store.ts +68 -0
- package/port/oracle/pi-acp/src/acp/session.ts +1078 -0
- package/port/oracle/pi-acp/src/acp/slash-commands.ts +197 -0
- package/port/oracle/pi-acp/src/acp/translate/bash.ts +100 -0
- package/port/oracle/pi-acp/src/acp/translate/pi-messages.ts +17 -0
- package/port/oracle/pi-acp/src/acp/translate/pi-tools.ts +51 -0
- package/port/oracle/pi-acp/src/acp/translate/prompt.ts +71 -0
- package/port/oracle/pi-acp/src/index.ts +82 -0
- package/port/oracle/pi-acp/src/pi-rpc/command.ts +16 -0
- package/port/oracle/pi-acp/src/pi-rpc/process.ts +432 -0
- package/port/oracle/pi-acp/test/component/agent-steering-followup-modes.test.ts +143 -0
- package/port/oracle/pi-acp/test/component/session-diff.test.ts +214 -0
- package/port/oracle/pi-acp/test/component/session-events.test.ts +1102 -0
- package/port/oracle/pi-acp/test/component/session-list-and-load.test.ts +129 -0
- package/port/oracle/pi-acp/test/component/session-list-custom-session-dir.test.ts +52 -0
- package/port/oracle/pi-acp/test/component/session-list-scoped.test.ts +76 -0
- package/port/oracle/pi-acp/test/component/session-load-toolresult.test.ts +68 -0
- package/port/oracle/pi-acp/test/component/session-queue-cancel.test.ts +43 -0
- package/port/oracle/pi-acp/test/component/session-slash-commands.test.ts +37 -0
- package/port/oracle/pi-acp/test/component/session-thinking-modes.test.ts +22 -0
- package/port/oracle/pi-acp/test/component/session-title-long-session.test.ts +56 -0
- package/port/oracle/pi-acp/test/component/session-updatedAt-message-only.test.ts +57 -0
- package/port/oracle/pi-acp/test/helpers/fakes.ts +87 -0
- package/port/oracle/pi-acp/test/unit/auth-methods-terminal-auth-meta.test.ts +22 -0
- package/port/oracle/pi-acp/test/unit/builtin-commands.test.ts +60 -0
- package/port/oracle/pi-acp/test/unit/context-usage.test.ts +204 -0
- package/port/oracle/pi-acp/test/unit/merge-commands.test.ts +19 -0
- package/port/oracle/pi-acp/test/unit/model-thinking-levels.test.ts +268 -0
- package/port/oracle/pi-acp/test/unit/new-session-auth-required-when-no-models.test.ts +51 -0
- package/port/oracle/pi-acp/test/unit/new-session-pi-not-found.test.ts +26 -0
- package/port/oracle/pi-acp/test/unit/new-session-runtime-startup-errors.test.ts +148 -0
- package/port/oracle/pi-acp/test/unit/pi-command.test.ts +44 -0
- package/port/oracle/pi-acp/test/unit/pi-commands.test.ts +32 -0
- package/port/oracle/pi-acp/test/unit/pi-enable-embed-context-flag.test.ts +35 -0
- package/port/oracle/pi-acp/test/unit/pi-messages.test.ts +29 -0
- package/port/oracle/pi-acp/test/unit/pi-rpc-request-timeout.test.ts +137 -0
- package/port/oracle/pi-acp/test/unit/pi-rpc-session-path.test.ts +93 -0
- package/port/oracle/pi-acp/test/unit/pi-tools.test.ts +40 -0
- package/port/oracle/pi-acp/test/unit/prompt-to-pi-message.test.ts +74 -0
- package/port/oracle/pi-acp/test/unit/session-config-options.test.ts +217 -0
- package/port/oracle/pi-acp/test/unit/session-delete.test.ts +170 -0
- package/port/oracle/pi-acp/test/unit/session-restore.test.ts +239 -0
- package/port/oracle/pi-acp/test/unit/slash-commands.test.ts +36 -0
- package/port/oracle/pi-acp/test/unit/startup-info-env.test.ts +84 -0
- package/port/oracle/pi-acp/test/unit/startup-info-load-session.test.ts +53 -0
- package/port/oracle/pi-acp/test/unit/startup-info-project-packages.test.ts +74 -0
- package/port/oracle/pi-acp/test/unit/stdout-destroyed-does-not-crash.test.ts +32 -0
- package/port/oracle/pi-acp/test/unit/thinking-level-rpc.test.ts +43 -0
- package/port/schema/LICENSE +191 -0
- package/port/schema/schema.json +8015 -0
- package/port/upstream-tests.json +201 -0
- package/provenance.json +28 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { PiAcpAgent } from '../../src/acp/agent.js'
|
|
4
|
+
import { FakeAgentSideConnection, FakePiRpcProcess, asAgentConn } from '../helpers/fakes.js'
|
|
5
|
+
|
|
6
|
+
class FakeSessions {
|
|
7
|
+
constructor(private readonly session: any) {}
|
|
8
|
+
maybeGet(_id: string) {
|
|
9
|
+
return this.session
|
|
10
|
+
}
|
|
11
|
+
get(_id: string) {
|
|
12
|
+
return this.session
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
test('PiAcpAgent: /steering is handled adapter-side', async () => {
|
|
17
|
+
const conn = new FakeAgentSideConnection()
|
|
18
|
+
const proc = new FakePiRpcProcess() as any
|
|
19
|
+
proc.getState = async () => ({ steeringMode: 'one-at-a-time' })
|
|
20
|
+
|
|
21
|
+
const agent = new PiAcpAgent(asAgentConn(conn))
|
|
22
|
+
;(agent as any).sessions = new FakeSessions({ sessionId: 's1', proc, fileCommands: [] }) as any
|
|
23
|
+
|
|
24
|
+
const res = await agent.prompt({
|
|
25
|
+
sessionId: 's1',
|
|
26
|
+
prompt: [{ type: 'text', text: '/steering' }]
|
|
27
|
+
} as any)
|
|
28
|
+
|
|
29
|
+
assert.equal(res.stopReason, 'end_turn')
|
|
30
|
+
assert.equal(proc.prompts.length, 0)
|
|
31
|
+
const last = conn.updates.at(-1)
|
|
32
|
+
assert.match((last as any).update.content.text, /Steering mode: one-at-a-time/)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('PiAcpAgent: /name sets session display name adapter-side', async () => {
|
|
36
|
+
const conn = new FakeAgentSideConnection()
|
|
37
|
+
const proc = new FakePiRpcProcess() as any
|
|
38
|
+
|
|
39
|
+
let setTo: string | null = null
|
|
40
|
+
proc.setSessionName = async (name: string) => {
|
|
41
|
+
setTo = name
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const agent = new PiAcpAgent(asAgentConn(conn))
|
|
45
|
+
;(agent as any).sessions = new FakeSessions({ sessionId: 's1', proc, fileCommands: [] }) as any
|
|
46
|
+
|
|
47
|
+
const res = await agent.prompt({
|
|
48
|
+
sessionId: 's1',
|
|
49
|
+
prompt: [{ type: 'text', text: '/name My Session' }]
|
|
50
|
+
} as any)
|
|
51
|
+
|
|
52
|
+
assert.equal(res.stopReason, 'end_turn')
|
|
53
|
+
assert.equal(proc.prompts.length, 0)
|
|
54
|
+
assert.equal(setTo, 'My Session')
|
|
55
|
+
const info = conn.updates.find(u => (u as any).update?.sessionUpdate === 'session_info_update')
|
|
56
|
+
assert.equal((info as any)?.update?.title, 'My Session')
|
|
57
|
+
|
|
58
|
+
const last = conn.updates.at(-1)
|
|
59
|
+
assert.match((last as any).update.content.text, /Session name set: My Session/)
|
|
60
|
+
})
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { PiAcpAgent } from '../../src/acp/agent.js'
|
|
4
|
+
import { PiAcpSession } from '../../src/acp/session.js'
|
|
5
|
+
import { SESSION_STATS_TIMEOUT_MS } from '../../src/pi-rpc/process.js'
|
|
6
|
+
import { FakeAgentSideConnection, FakePiRpcProcess, asAgentConn } from '../helpers/fakes.js'
|
|
7
|
+
|
|
8
|
+
class FakeSessions {
|
|
9
|
+
constructor(private readonly session: any) {}
|
|
10
|
+
|
|
11
|
+
async create() {
|
|
12
|
+
return this.session
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
maybeGet(sessionId: string) {
|
|
16
|
+
if (sessionId !== this.session.sessionId) return undefined
|
|
17
|
+
return this.session
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get(sessionId: string) {
|
|
21
|
+
if (sessionId !== this.session.sessionId) {
|
|
22
|
+
throw new Error(`Unknown sessionId: ${sessionId}`)
|
|
23
|
+
}
|
|
24
|
+
return this.session
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Let the `setTimeout(..., 0)` continuations scheduled after session/new or session/load run. */
|
|
29
|
+
async function drainScheduledWork(): Promise<void> {
|
|
30
|
+
for (let i = 0; i < 5; i += 1) await new Promise(resolve => setTimeout(resolve, 0))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function makeSession(proc: FakePiRpcProcess, conn: FakeAgentSideConnection): PiAcpSession {
|
|
34
|
+
return new PiAcpSession({
|
|
35
|
+
sessionId: 's1',
|
|
36
|
+
cwd: process.cwd(),
|
|
37
|
+
mcpServers: [],
|
|
38
|
+
proc: proc as any,
|
|
39
|
+
conn: asAgentConn(conn),
|
|
40
|
+
fileCommands: []
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
test('PiAcpSession: context usage request specifies the auxiliary timeout', async () => {
|
|
45
|
+
const conn = new FakeAgentSideConnection()
|
|
46
|
+
const proc = new FakePiRpcProcess()
|
|
47
|
+
let requestedTimeout: number | undefined
|
|
48
|
+
proc.getSessionStats = async (timeoutMs?: number) => {
|
|
49
|
+
requestedTimeout = timeoutMs
|
|
50
|
+
return { contextUsage: { tokens: 100, contextWindow: 100_000 } }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await makeSession(proc, conn).publishContextUsage()
|
|
54
|
+
|
|
55
|
+
assert.equal(requestedTimeout, SESSION_STATS_TIMEOUT_MS)
|
|
56
|
+
assert.deepEqual(conn.updates, [
|
|
57
|
+
{ sessionId: 's1', update: { sessionUpdate: 'usage_update', used: 100, size: 100_000 } }
|
|
58
|
+
])
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('PiAcpAgent: newSession publishes context usage only after the response is returned', async () => {
|
|
62
|
+
const conn = new FakeAgentSideConnection()
|
|
63
|
+
const proc = new FakePiRpcProcess()
|
|
64
|
+
proc.sessionStats = { contextUsage: { tokens: 1_234, contextWindow: 100_000 } }
|
|
65
|
+
proc.getState = async () => ({ thinkingLevel: 'medium' })
|
|
66
|
+
const session = makeSession(proc, conn)
|
|
67
|
+
|
|
68
|
+
const agent = new PiAcpAgent(asAgentConn(conn), {} as any)
|
|
69
|
+
;(agent as any).sessions = new FakeSessions(session) as any
|
|
70
|
+
|
|
71
|
+
const result = await agent.newSession({ cwd: process.cwd(), mcpServers: [] } as any)
|
|
72
|
+
assert.equal(result.sessionId, 's1')
|
|
73
|
+
assert.equal(
|
|
74
|
+
conn.updates.some(u => u.update.sessionUpdate === 'usage_update'),
|
|
75
|
+
false
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
await drainScheduledWork()
|
|
79
|
+
|
|
80
|
+
assert.deepEqual(
|
|
81
|
+
conn.updates.filter(u => u.update.sessionUpdate === 'usage_update'),
|
|
82
|
+
[{ sessionId: 's1', update: { sessionUpdate: 'usage_update', used: 1_234, size: 100_000 } }]
|
|
83
|
+
)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('PiAcpAgent: newSession tolerates a failing get_session_stats', async () => {
|
|
87
|
+
const conn = new FakeAgentSideConnection()
|
|
88
|
+
const proc = new FakePiRpcProcess()
|
|
89
|
+
proc.sessionStatsError = new Error('pi get_session_stats failed: unsupported')
|
|
90
|
+
proc.getState = async () => ({ thinkingLevel: 'medium' })
|
|
91
|
+
const session = makeSession(proc, conn)
|
|
92
|
+
|
|
93
|
+
const agent = new PiAcpAgent(asAgentConn(conn), {} as any)
|
|
94
|
+
;(agent as any).sessions = new FakeSessions(session) as any
|
|
95
|
+
|
|
96
|
+
const result = await agent.newSession({ cwd: process.cwd(), mcpServers: [] } as any)
|
|
97
|
+
assert.equal(result.sessionId, 's1')
|
|
98
|
+
|
|
99
|
+
await drainScheduledWork()
|
|
100
|
+
|
|
101
|
+
assert.equal(
|
|
102
|
+
conn.updates.some(u => u.update.sessionUpdate === 'usage_update'),
|
|
103
|
+
false
|
|
104
|
+
)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('PiAcpAgent: switching the model config option refreshes context usage', async () => {
|
|
108
|
+
const conn = new FakeAgentSideConnection()
|
|
109
|
+
const proc = new FakePiRpcProcess()
|
|
110
|
+
const state: { thinkingLevel: string; model: { provider: string; id: string } } = {
|
|
111
|
+
thinkingLevel: 'medium',
|
|
112
|
+
model: { provider: 'test', id: 'alpha' }
|
|
113
|
+
}
|
|
114
|
+
proc.getAvailableModels = async () => ({
|
|
115
|
+
models: [
|
|
116
|
+
{ provider: 'test', id: 'alpha', name: 'Alpha' },
|
|
117
|
+
{ provider: 'test', id: 'beta', name: 'Beta' }
|
|
118
|
+
]
|
|
119
|
+
})
|
|
120
|
+
proc.getState = async () => state
|
|
121
|
+
;(proc as any).setModel = async (provider: string, modelId: string) => {
|
|
122
|
+
state.model = { provider, id: modelId }
|
|
123
|
+
proc.sessionStats = { contextUsage: { tokens: 500, contextWindow: modelId === 'beta' ? 200_000 : 100_000 } }
|
|
124
|
+
}
|
|
125
|
+
proc.sessionStats = { contextUsage: { tokens: 500, contextWindow: 100_000 } }
|
|
126
|
+
|
|
127
|
+
const session = makeSession(proc, conn)
|
|
128
|
+
const agent = new PiAcpAgent(asAgentConn(conn), {} as any)
|
|
129
|
+
;(agent as any).sessions = new FakeSessions(session) as any
|
|
130
|
+
|
|
131
|
+
await agent.setSessionConfigOption({ sessionId: 's1', configId: 'model', value: 'test/beta' } as any)
|
|
132
|
+
|
|
133
|
+
assert.deepEqual(
|
|
134
|
+
conn.updates.map(u => u.update.sessionUpdate),
|
|
135
|
+
['current_mode_update', 'config_option_update', 'usage_update']
|
|
136
|
+
)
|
|
137
|
+
assert.deepEqual(conn.updates.at(-1), {
|
|
138
|
+
sessionId: 's1',
|
|
139
|
+
update: { sessionUpdate: 'usage_update', used: 500, size: 200_000 }
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('PiAcpAgent: unstable_setSessionModel refreshes context usage', async () => {
|
|
144
|
+
const conn = new FakeAgentSideConnection()
|
|
145
|
+
const proc = new FakePiRpcProcess()
|
|
146
|
+
const state: { thinkingLevel: string; model: { provider: string; id: string } } = {
|
|
147
|
+
thinkingLevel: 'medium',
|
|
148
|
+
model: { provider: 'test', id: 'alpha' }
|
|
149
|
+
}
|
|
150
|
+
proc.getAvailableModels = async () => ({
|
|
151
|
+
models: [
|
|
152
|
+
{ provider: 'test', id: 'alpha', name: 'Alpha' },
|
|
153
|
+
{ provider: 'test', id: 'beta', name: 'Beta' }
|
|
154
|
+
]
|
|
155
|
+
})
|
|
156
|
+
proc.getState = async () => state
|
|
157
|
+
;(proc as any).setModel = async (provider: string, modelId: string) => {
|
|
158
|
+
state.model = { provider, id: modelId }
|
|
159
|
+
proc.sessionStats = { contextUsage: { tokens: 700, contextWindow: modelId === 'beta' ? 200_000 : 100_000 } }
|
|
160
|
+
}
|
|
161
|
+
proc.sessionStats = { contextUsage: { tokens: 700, contextWindow: 100_000 } }
|
|
162
|
+
|
|
163
|
+
const session = makeSession(proc, conn)
|
|
164
|
+
const agent = new PiAcpAgent(asAgentConn(conn), {} as any)
|
|
165
|
+
;(agent as any).sessions = new FakeSessions(session) as any
|
|
166
|
+
|
|
167
|
+
await agent.unstable_setSessionModel({ sessionId: 's1', modelId: 'test/beta' })
|
|
168
|
+
|
|
169
|
+
assert.equal(proc.getSessionStatsCount, 1)
|
|
170
|
+
assert.deepEqual(
|
|
171
|
+
conn.updates.map(u => u.update.sessionUpdate),
|
|
172
|
+
['current_mode_update', 'config_option_update', 'usage_update']
|
|
173
|
+
)
|
|
174
|
+
assert.deepEqual(conn.updates.at(-1), {
|
|
175
|
+
sessionId: 's1',
|
|
176
|
+
update: { sessionUpdate: 'usage_update', used: 700, size: 200_000 }
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('PiAcpAgent: switching the thinking level does not publish context usage', async () => {
|
|
181
|
+
const conn = new FakeAgentSideConnection()
|
|
182
|
+
const proc = new FakePiRpcProcess()
|
|
183
|
+
const state: { thinkingLevel: string; model: { provider: string; id: string } } = {
|
|
184
|
+
thinkingLevel: 'medium',
|
|
185
|
+
model: { provider: 'test', id: 'alpha' }
|
|
186
|
+
}
|
|
187
|
+
proc.getState = async () => state
|
|
188
|
+
;(proc as any).setThinkingLevel = async (level: string) => {
|
|
189
|
+
state.thinkingLevel = level
|
|
190
|
+
}
|
|
191
|
+
proc.sessionStats = { contextUsage: { tokens: 500, contextWindow: 100_000 } }
|
|
192
|
+
|
|
193
|
+
const session = makeSession(proc, conn)
|
|
194
|
+
const agent = new PiAcpAgent(asAgentConn(conn), {} as any)
|
|
195
|
+
;(agent as any).sessions = new FakeSessions(session) as any
|
|
196
|
+
|
|
197
|
+
await agent.setSessionConfigOption({ sessionId: 's1', configId: 'thought_level', value: 'high' } as any)
|
|
198
|
+
|
|
199
|
+
assert.equal(proc.getSessionStatsCount, 0)
|
|
200
|
+
assert.equal(
|
|
201
|
+
conn.updates.some(u => u.update.sessionUpdate === 'usage_update'),
|
|
202
|
+
false
|
|
203
|
+
)
|
|
204
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
// Minimal local impl (mirrors src/acp/agent.ts behavior)
|
|
5
|
+
function mergeCommands(a: Array<{ name: string }>, b: Array<{ name: string }>) {
|
|
6
|
+
const out: Array<{ name: string }> = []
|
|
7
|
+
const seen = new Set<string>()
|
|
8
|
+
for (const c of [...a, ...b]) {
|
|
9
|
+
if (seen.has(c.name)) continue
|
|
10
|
+
seen.add(c.name)
|
|
11
|
+
out.push(c)
|
|
12
|
+
}
|
|
13
|
+
return out
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
test('mergeCommands: preserves order and de-dupes (first wins)', () => {
|
|
17
|
+
const res = mergeCommands([{ name: 'a' }, { name: 'b' }], [{ name: 'b' }, { name: 'c' }])
|
|
18
|
+
assert.deepEqual(res, [{ name: 'a' }, { name: 'b' }, { name: 'c' }])
|
|
19
|
+
})
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { SessionManager } from '../../src/acp/session.js'
|
|
7
|
+
import { SessionStore } from '../../src/acp/session-store.js'
|
|
8
|
+
import { PiAcpAgent } from '../../src/acp/agent.js'
|
|
9
|
+
import { PiRpcProcess } from '../../src/pi-rpc/process.js'
|
|
10
|
+
import { FakeAgentSideConnection, asAgentConn } from '../helpers/fakes.js'
|
|
11
|
+
|
|
12
|
+
function fixture() {
|
|
13
|
+
const conn = new FakeAgentSideConnection()
|
|
14
|
+
const state = { thinkingLevel: 'max', model: { provider: 'test', id: 'reasoning' } }
|
|
15
|
+
const calls: string[] = []
|
|
16
|
+
const proc = {
|
|
17
|
+
onEvent: () => () => {},
|
|
18
|
+
getMessages: async () => ({ messages: [] }),
|
|
19
|
+
getState: async () => state,
|
|
20
|
+
getAvailableModels: async () => ({
|
|
21
|
+
models: [
|
|
22
|
+
{ provider: 'test', id: 'reasoning' },
|
|
23
|
+
{ provider: 'test', id: 'plain' }
|
|
24
|
+
]
|
|
25
|
+
}),
|
|
26
|
+
getAvailableThinkingLevels: async () => (state.model.id === 'plain' ? ['off'] : ['low', 'high', 'max']),
|
|
27
|
+
async setThinkingLevel(level: string) {
|
|
28
|
+
calls.push(level)
|
|
29
|
+
state.thinkingLevel = 'max'
|
|
30
|
+
},
|
|
31
|
+
async setModel(provider: string, id: string) {
|
|
32
|
+
calls.push(id)
|
|
33
|
+
state.model = { provider, id }
|
|
34
|
+
state.thinkingLevel = id === 'plain' ? 'off' : 'max'
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const session = {
|
|
38
|
+
sessionId: 's1',
|
|
39
|
+
cwd: process.cwd(),
|
|
40
|
+
proc,
|
|
41
|
+
setStartupInfo() {},
|
|
42
|
+
sendStartupInfoIfPending() {},
|
|
43
|
+
async publishContextUsage() {}
|
|
44
|
+
}
|
|
45
|
+
const agent = new PiAcpAgent(asAgentConn(conn))
|
|
46
|
+
Reflect.set(agent, 'sessions', {
|
|
47
|
+
maybeGet: () => session,
|
|
48
|
+
get: () => session,
|
|
49
|
+
create: async () => session,
|
|
50
|
+
close() {}
|
|
51
|
+
})
|
|
52
|
+
return { agent, conn, proc, state, calls }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function assertUpdates(conn: FakeAgentSideConnection, level: string, levels: string[]) {
|
|
56
|
+
assert.equal(conn.updates.length, 2)
|
|
57
|
+
assert.deepEqual(conn.updates[0], {
|
|
58
|
+
sessionId: 's1',
|
|
59
|
+
update: { sessionUpdate: 'current_mode_update', currentModeId: level }
|
|
60
|
+
})
|
|
61
|
+
const update = conn.updates[1].update as {
|
|
62
|
+
configOptions: Array<{ id: string; currentValue: string; options: Array<{ value: string }> }>
|
|
63
|
+
}
|
|
64
|
+
const option = update.configOptions.find(option => option.id === 'thought_level')!
|
|
65
|
+
assert.equal(option.currentValue, level)
|
|
66
|
+
assert.deepEqual(
|
|
67
|
+
option.options.map(option => option.value),
|
|
68
|
+
levels
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const legacy of [false, true]) {
|
|
73
|
+
for (const requested of ['max', 'xhigh', 'ordinary', ' mean ', ' ']) {
|
|
74
|
+
test(`${legacy ? 'legacy' : 'config'} reasoning setter reports actual max for ${requested}`, async () => {
|
|
75
|
+
const { agent, conn, calls } = fixture()
|
|
76
|
+
if (legacy) await agent.setSessionMode({ sessionId: 's1', modeId: requested })
|
|
77
|
+
else {
|
|
78
|
+
const result = await agent.setSessionConfigOption({
|
|
79
|
+
sessionId: 's1',
|
|
80
|
+
configId: 'thought_level',
|
|
81
|
+
value: requested
|
|
82
|
+
})
|
|
83
|
+
assert.equal(result.configOptions.find(option => option.id === 'thought_level')?.currentValue, 'max')
|
|
84
|
+
}
|
|
85
|
+
assert.deepEqual(calls, [requested])
|
|
86
|
+
assertUpdates(conn, 'max', ['low', 'high', 'max'])
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
test(`${legacy ? 'legacy' : 'config'} reasoning setter reports an opaque applied level`, async () => {
|
|
90
|
+
const { agent, conn, proc, state, calls } = fixture()
|
|
91
|
+
proc.getAvailableThinkingLevels = async () => ['ordinary', 'mean']
|
|
92
|
+
proc.setThinkingLevel = async level => {
|
|
93
|
+
calls.push(level)
|
|
94
|
+
state.thinkingLevel = 'mean'
|
|
95
|
+
}
|
|
96
|
+
if (legacy) await agent.setSessionMode({ sessionId: 's1', modeId: 'ordinary' })
|
|
97
|
+
else {
|
|
98
|
+
const result = await agent.setSessionConfigOption({
|
|
99
|
+
sessionId: 's1',
|
|
100
|
+
configId: 'thought_level',
|
|
101
|
+
value: 'ordinary'
|
|
102
|
+
})
|
|
103
|
+
assert.equal(result.configOptions.find(option => option.id === 'thought_level')?.currentValue, 'mean')
|
|
104
|
+
}
|
|
105
|
+
assert.deepEqual(calls, ['ordinary'])
|
|
106
|
+
assertUpdates(conn, 'mean', ['ordinary', 'mean'])
|
|
107
|
+
})
|
|
108
|
+
test(`${legacy ? 'legacy' : 'config'} model setter refreshes model-specific levels and current mode`, async () => {
|
|
109
|
+
const { agent, conn } = fixture()
|
|
110
|
+
for (const model of ['plain', 'reasoning']) {
|
|
111
|
+
conn.updates.length = 0
|
|
112
|
+
if (legacy) await agent.unstable_setSessionModel({ sessionId: 's1', modelId: `test/${model}` })
|
|
113
|
+
else await agent.setSessionConfigOption({ sessionId: 's1', configId: 'model', value: `test/${model}` })
|
|
114
|
+
assertUpdates(conn, model === 'plain' ? 'off' : 'max', model === 'plain' ? ['off'] : ['low', 'high', 'max'])
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
for (const failure of ['state', 'discovery', 'empty', 'non-string', 'inconsistent']) {
|
|
118
|
+
test(`${legacy ? 'legacy' : 'config'} reasoning setter emits no success on ${failure} read failure`, async () => {
|
|
119
|
+
const { agent, conn, proc, state } = fixture()
|
|
120
|
+
if (failure === 'state')
|
|
121
|
+
proc.getState = async () => {
|
|
122
|
+
throw new Error('state failed')
|
|
123
|
+
}
|
|
124
|
+
if (failure === 'discovery')
|
|
125
|
+
proc.getAvailableThinkingLevels = async () => {
|
|
126
|
+
throw new Error('discovery failed')
|
|
127
|
+
}
|
|
128
|
+
if (failure === 'empty' || failure === 'non-string' || failure === 'inconsistent')
|
|
129
|
+
proc.setThinkingLevel = async () => {
|
|
130
|
+
Reflect.set(state, 'thinkingLevel', failure === 'empty' ? '' : failure === 'non-string' ? 1 : 'medium')
|
|
131
|
+
}
|
|
132
|
+
await assert.rejects(
|
|
133
|
+
legacy
|
|
134
|
+
? agent.setSessionMode({ sessionId: 's1', modeId: 'max' })
|
|
135
|
+
: agent.setSessionConfigOption({ sessionId: 's1', configId: 'thought_level', value: 'max' })
|
|
136
|
+
)
|
|
137
|
+
assert.deepEqual(conn.updates, [])
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
test('invalid configuration and legacy mode requests do not mutate Pi', async () => {
|
|
143
|
+
const { agent, calls, conn } = fixture()
|
|
144
|
+
for (const request of [
|
|
145
|
+
{ configId: 'thought_level', value: '' },
|
|
146
|
+
{ configId: 'unknown', value: 'high' },
|
|
147
|
+
{ configId: 'thought_level', value: 1 }
|
|
148
|
+
]) {
|
|
149
|
+
await assert.rejects(
|
|
150
|
+
agent.setSessionConfigOption({ sessionId: 's1', ...request } as Parameters<
|
|
151
|
+
PiAcpAgent['setSessionConfigOption']
|
|
152
|
+
>[0]),
|
|
153
|
+
{ code: -32602 }
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
for (const modeId of ['', 1, null]) {
|
|
157
|
+
await assert.rejects(
|
|
158
|
+
agent.setSessionMode({ sessionId: 's1', modeId } as Parameters<PiAcpAgent['setSessionMode']>[0]),
|
|
159
|
+
{ code: -32602 }
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
assert.deepEqual(calls, [])
|
|
163
|
+
assert.deepEqual(conn.updates, [])
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
for (const load of [false, true]) {
|
|
167
|
+
for (const levels of [['off'], ['low', 'high', 'max'], ['ordinary', 'mean']]) {
|
|
168
|
+
test(`${load ? 'load' : 'new'} session advertises exact ${levels} levels`, async t => {
|
|
169
|
+
const { agent, proc, state } = fixture()
|
|
170
|
+
state.thinkingLevel = levels[levels.length - 1]
|
|
171
|
+
proc.getAvailableThinkingLevels = async () => levels
|
|
172
|
+
t.mock.method(globalThis, 'setTimeout', () => 0 as unknown as ReturnType<typeof setTimeout>)
|
|
173
|
+
t.mock.method(PiRpcProcess, 'spawn', async () => proc as unknown as PiRpcProcess)
|
|
174
|
+
Reflect.set(agent, 'store', {
|
|
175
|
+
get: () => ({ cwd: process.cwd(), sessionFile: '/tmp/thinking-test.jsonl' }),
|
|
176
|
+
upsert() {},
|
|
177
|
+
delete() {}
|
|
178
|
+
})
|
|
179
|
+
const result = load
|
|
180
|
+
? await agent.loadSession({ sessionId: 's1', cwd: process.cwd(), mcpServers: [] })
|
|
181
|
+
: await agent.newSession({ cwd: process.cwd(), mcpServers: [] })
|
|
182
|
+
assert.equal(result.modes?.currentModeId, state.thinkingLevel)
|
|
183
|
+
assert.deepEqual(
|
|
184
|
+
result.modes?.availableModes.map(mode => mode.id),
|
|
185
|
+
levels
|
|
186
|
+
)
|
|
187
|
+
const option = result.configOptions?.find(option => option.id === 'thought_level')
|
|
188
|
+
assert.equal(option?.currentValue, state.thinkingLevel)
|
|
189
|
+
assert.deepEqual(
|
|
190
|
+
option?.options.map(option => ('value' in option ? option.value : undefined)),
|
|
191
|
+
levels
|
|
192
|
+
)
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for (const failure of ['discovery', 'missing-current', 'inconsistent-current', 'state']) {
|
|
198
|
+
test(`load configuration ${failure} failure closes restored child and preserves history for retry`, async t => {
|
|
199
|
+
const root = mkdtempSync(join(tmpdir(), 'pi-acp-load-thinking-'))
|
|
200
|
+
t.after(() => rmSync(root, { recursive: true, force: true }))
|
|
201
|
+
t.mock.method(globalThis, 'setTimeout', () => 0 as unknown as ReturnType<typeof setTimeout>)
|
|
202
|
+
const sessionFile = join(root, 'history.jsonl')
|
|
203
|
+
writeFileSync(sessionFile, 'persisted history\n')
|
|
204
|
+
const store = new SessionStore(join(root, 'map.json'))
|
|
205
|
+
store.upsert({ sessionId: 's1', cwd: root, sessionFile })
|
|
206
|
+
const { agent, conn, proc } = fixture()
|
|
207
|
+
const sessions = new SessionManager()
|
|
208
|
+
Reflect.set(agent, 'sessions', sessions)
|
|
209
|
+
Reflect.set(agent, 'store', store)
|
|
210
|
+
let existingDisposed = 0
|
|
211
|
+
let restoredDisposed = 0
|
|
212
|
+
let historyReads = 0
|
|
213
|
+
let shouldFail = true
|
|
214
|
+
let spawns = 0
|
|
215
|
+
sessions.getOrCreate('existing', {
|
|
216
|
+
cwd: root,
|
|
217
|
+
mcpServers: [],
|
|
218
|
+
conn: asAgentConn(conn),
|
|
219
|
+
proc: {
|
|
220
|
+
onEvent: () => () => {},
|
|
221
|
+
dispose() {
|
|
222
|
+
existingDisposed++
|
|
223
|
+
}
|
|
224
|
+
} as unknown as PiRpcProcess
|
|
225
|
+
})
|
|
226
|
+
t.after(() => sessions.disposeAll())
|
|
227
|
+
t.mock.method(PiRpcProcess, 'spawn', async () => {
|
|
228
|
+
spawns++
|
|
229
|
+
return {
|
|
230
|
+
...proc,
|
|
231
|
+
dispose() {
|
|
232
|
+
restoredDisposed++
|
|
233
|
+
},
|
|
234
|
+
async getState() {
|
|
235
|
+
if (shouldFail && failure === 'state') throw new Error('state unavailable')
|
|
236
|
+
if (shouldFail && failure === 'missing-current') return {}
|
|
237
|
+
if (shouldFail && failure === 'inconsistent-current') return { thinkingLevel: 'medium' }
|
|
238
|
+
return proc.getState()
|
|
239
|
+
},
|
|
240
|
+
async getAvailableThinkingLevels() {
|
|
241
|
+
if (shouldFail && failure === 'discovery') throw new Error('discovery unavailable')
|
|
242
|
+
return proc.getAvailableThinkingLevels()
|
|
243
|
+
},
|
|
244
|
+
async getMessages() {
|
|
245
|
+
historyReads++
|
|
246
|
+
return { messages: [{ role: 'user', content: 'persisted prompt' }] }
|
|
247
|
+
}
|
|
248
|
+
} as unknown as PiRpcProcess
|
|
249
|
+
})
|
|
250
|
+
await assert.rejects(agent.loadSession({ sessionId: 's1', cwd: root, mcpServers: [] }))
|
|
251
|
+
assert.equal(restoredDisposed, 1)
|
|
252
|
+
assert.equal(sessions.maybeGet('s1'), undefined)
|
|
253
|
+
assert.ok(sessions.maybeGet('existing'))
|
|
254
|
+
assert.equal(existingDisposed, 0)
|
|
255
|
+
assert.equal(historyReads, 0)
|
|
256
|
+
assert.equal(conn.updates.length, 0)
|
|
257
|
+
assert.equal(readFileSync(sessionFile, 'utf8'), 'persisted history\n')
|
|
258
|
+
assert.equal(store.get('s1')?.sessionFile, sessionFile)
|
|
259
|
+
shouldFail = false
|
|
260
|
+
const result = await agent.loadSession({ sessionId: 's1', cwd: root, mcpServers: [] })
|
|
261
|
+
assert.equal(result.modes?.currentModeId, 'max')
|
|
262
|
+
assert.equal(spawns, 2)
|
|
263
|
+
assert.equal(restoredDisposed, 1)
|
|
264
|
+
assert.equal(historyReads, 1)
|
|
265
|
+
assert.ok(conn.updates.some(update => update.update.sessionUpdate === 'user_message_chunk'))
|
|
266
|
+
assert.equal(readFileSync(sessionFile, 'utf8'), 'persisted history\n')
|
|
267
|
+
})
|
|
268
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { PiAcpAgent } from '../../src/acp/agent.js'
|
|
4
|
+
import { FakeAgentSideConnection, asAgentConn } from '../helpers/fakes.js'
|
|
5
|
+
|
|
6
|
+
class FakeSessions {
|
|
7
|
+
closeCalls: string[] = []
|
|
8
|
+
|
|
9
|
+
constructor(private readonly session: any) {}
|
|
10
|
+
|
|
11
|
+
async create(_params: any) {
|
|
12
|
+
return this.session
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
close(sessionId: string) {
|
|
16
|
+
this.closeCalls.push(sessionId)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test('PiAcpAgent: newSession throws AUTH_REQUIRED when pi reports zero available models', async () => {
|
|
21
|
+
const conn = new FakeAgentSideConnection()
|
|
22
|
+
|
|
23
|
+
const session = {
|
|
24
|
+
sessionId: 's1',
|
|
25
|
+
cwd: process.cwd(),
|
|
26
|
+
proc: {
|
|
27
|
+
async getAvailableModels() {
|
|
28
|
+
return { models: [] }
|
|
29
|
+
},
|
|
30
|
+
async getState() {
|
|
31
|
+
return { thinkingLevel: 'medium', model: null }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const sessions = new FakeSessions(session)
|
|
37
|
+
const agent = new PiAcpAgent(asAgentConn(conn), {} as any)
|
|
38
|
+
;(agent as any).sessions = sessions as any
|
|
39
|
+
|
|
40
|
+
let threw = false
|
|
41
|
+
try {
|
|
42
|
+
await agent.newSession({ cwd: process.cwd(), mcpServers: [] } as any)
|
|
43
|
+
} catch (e: any) {
|
|
44
|
+
threw = true
|
|
45
|
+
assert.equal(e?.code, -32000)
|
|
46
|
+
assert.match(String(e?.message), /Configure an API key or log in with an OAuth provider/i)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
assert.equal(threw, true)
|
|
50
|
+
assert.deepEqual(sessions.closeCalls, ['s1'])
|
|
51
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { PiAcpAgent } from '../../src/acp/agent.js'
|
|
4
|
+
import { FakeAgentSideConnection, asAgentConn } from '../helpers/fakes.js'
|
|
5
|
+
|
|
6
|
+
test('PiAcpAgent: newSession returns a helpful Internal error when pi is not installed', async () => {
|
|
7
|
+
const prevPiCmd = process.env.PI_ACP_PI_COMMAND
|
|
8
|
+
process.env.PI_ACP_PI_COMMAND = 'pi-does-not-exist-12345'
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const conn = new FakeAgentSideConnection()
|
|
12
|
+
const agent = new PiAcpAgent(asAgentConn(conn), {} as any)
|
|
13
|
+
|
|
14
|
+
await assert.rejects(
|
|
15
|
+
() => agent.newSession({ cwd: process.cwd(), mcpServers: [] } as any),
|
|
16
|
+
(e: any) =>
|
|
17
|
+
e?.code === -32603 &&
|
|
18
|
+
String(e?.message ?? '')
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.includes('executable not found')
|
|
21
|
+
)
|
|
22
|
+
} finally {
|
|
23
|
+
if (prevPiCmd == null) delete process.env.PI_ACP_PI_COMMAND
|
|
24
|
+
else process.env.PI_ACP_PI_COMMAND = prevPiCmd
|
|
25
|
+
}
|
|
26
|
+
})
|