@oh-my-pi/pi-coding-agent 16.5.0 → 16.5.1
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/CHANGELOG.md +26 -0
- package/dist/cli.js +3336 -3318
- package/dist/types/advisor/advise-tool.d.ts +12 -1
- package/dist/types/advisor/runtime.d.ts +41 -1
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/update-cli.d.ts +4 -1
- package/dist/types/cli/usage-cli.d.ts +3 -0
- package/dist/types/cli/usage-error.d.ts +4 -0
- package/dist/types/config/api-key-resolver.d.ts +2 -2
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/model-resolver.d.ts +8 -1
- package/dist/types/config/models-config.d.ts +1 -1
- package/dist/types/eval/__tests__/process-entry-import.test.d.ts +1 -0
- package/dist/types/eval/bridge-timeout.d.ts +9 -1
- package/dist/types/eval/js/context-manager.d.ts +5 -3
- package/dist/types/eval/js/process-entry.d.ts +6 -0
- package/dist/types/eval/js/worker-core.d.ts +15 -1
- package/dist/types/eval/py/spawn-options.d.ts +10 -0
- package/dist/types/eval/py/tool-bridge.d.ts +1 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +3 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/internal-urls/memory-protocol.d.ts +6 -7
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/components/transcript-container.d.ts +3 -2
- package/dist/types/modes/magic-keyword-boundary.d.ts +9 -0
- package/dist/types/modes/orchestrate.d.ts +1 -1
- package/dist/types/modes/rpc/host-tools.d.ts +2 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +26 -6
- package/dist/types/modes/ultrathink.d.ts +1 -1
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +12 -0
- package/dist/types/modes/workflow.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +6 -0
- package/dist/types/session/exit-diagnostics.d.ts +11 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +11 -0
- package/dist/types/subprocess/worker-client.d.ts +6 -0
- package/dist/types/tools/bash-skill-urls.d.ts +1 -0
- package/dist/types/web/search/provider.d.ts +10 -3
- package/dist/types/web/search/providers/codex.d.ts +5 -4
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +830 -42
- package/src/advisor/advise-tool.ts +17 -1
- package/src/advisor/runtime.ts +288 -67
- package/src/autolearn/controller.ts +15 -3
- package/src/cli/args.ts +12 -0
- package/src/cli/auth-broker-cli.ts +30 -11
- package/src/cli/auth-gateway-cli.ts +5 -1
- package/src/cli/dry-balance-cli.ts +14 -4
- package/src/cli/flag-tables.ts +21 -7
- package/src/cli/update-cli.ts +62 -11
- package/src/cli/usage-cli.ts +58 -5
- package/src/cli/usage-error.ts +7 -0
- package/src/cli.ts +23 -1
- package/src/commands/acp.ts +11 -2
- package/src/commands/launch.ts +12 -3
- package/src/commands/token.ts +3 -1
- package/src/config/api-key-resolver.ts +12 -3
- package/src/config/config-file.ts +30 -12
- package/src/config/model-registry.ts +7 -7
- package/src/config/model-resolver.ts +21 -7
- package/src/config/models-config.ts +1 -1
- package/src/eval/__tests__/agent-bridge.test.ts +19 -14
- package/src/eval/__tests__/bridge-timeout.test.ts +106 -0
- package/src/eval/__tests__/js-context-manager.test.ts +158 -1
- package/src/eval/__tests__/kernel-spawn.test.ts +12 -0
- package/src/eval/__tests__/process-entry-import.test.ts +27 -0
- package/src/eval/agent-bridge.ts +121 -116
- package/src/eval/bridge-timeout.ts +20 -2
- package/src/eval/executor-base.ts +85 -7
- package/src/eval/jl/kernel.ts +2 -1
- package/src/eval/js/context-manager.ts +109 -32
- package/src/eval/js/process-entry.ts +27 -0
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/eval/js/worker-core.ts +70 -9
- package/src/eval/js/worker-entry.ts +1 -1
- package/src/eval/py/kernel.ts +2 -1
- package/src/eval/py/spawn-options.ts +13 -0
- package/src/eval/py/tool-bridge.ts +13 -14
- package/src/eval/rb/kernel.ts +2 -1
- package/src/extensibility/custom-tools/types.ts +3 -0
- package/src/extensibility/extensions/runner.ts +3 -0
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/plugins/manager.ts +21 -0
- package/src/internal-urls/memory-protocol.ts +13 -9
- package/src/lsp/client.ts +7 -1
- package/src/main.ts +29 -0
- package/src/mcp/tool-bridge.ts +57 -6
- package/src/modes/components/chat-transcript-builder.ts +22 -1
- package/src/modes/components/status-line/component.ts +10 -1
- package/src/modes/components/transcript-container.ts +110 -7
- package/src/modes/controllers/command-controller.ts +12 -4
- package/src/modes/controllers/event-controller.ts +80 -15
- package/src/modes/controllers/selector-controller.ts +15 -3
- package/src/modes/magic-keyword-boundary.ts +23 -0
- package/src/modes/orchestrate.ts +6 -5
- package/src/modes/print-mode.ts +9 -0
- package/src/modes/rpc/host-tools.ts +15 -0
- package/src/modes/rpc/rpc-mode.ts +123 -48
- package/src/modes/ultrathink.ts +6 -5
- package/src/modes/utils/transcript-render-helpers.ts +54 -0
- package/src/modes/utils/ui-helpers.ts +27 -1
- package/src/modes/workflow.ts +6 -5
- package/src/prompts/advisor/system.md +1 -0
- package/src/sdk.ts +33 -3
- package/src/session/agent-session.ts +239 -17
- package/src/session/exit-diagnostics.ts +108 -0
- package/src/session/streaming-output.ts +40 -12
- package/src/slash-commands/helpers/active-oauth-account.ts +22 -2
- package/src/slash-commands/helpers/logout.ts +23 -3
- package/src/slash-commands/helpers/usage-report.ts +14 -2
- package/src/subprocess/worker-client.ts +9 -2
- package/src/task/executor.ts +8 -0
- package/src/task/render.test.ts +36 -0
- package/src/task/render.ts +55 -43
- package/src/tools/bash-skill-urls.ts +4 -1
- package/src/tools/bash.ts +1 -0
- package/src/tools/write.ts +82 -9
- package/src/tools/yield.ts +29 -1
- package/src/web/search/index.ts +39 -22
- package/src/web/search/provider.ts +33 -16
- package/src/web/search/providers/codex.ts +68 -21
|
@@ -9,6 +9,17 @@ function normalizeIdentityValue(value: unknown): string | undefined {
|
|
|
9
9
|
* True when a single usage-limit column belongs to the given OAuth identity.
|
|
10
10
|
*
|
|
11
11
|
* Single definition of the matching rules for both `/usage` renderers:
|
|
12
|
+
* - `orgId` ↔ report metadata `orgId` — a GATE that QUALIFIES the base
|
|
13
|
+
* identity, never a replacement for it. Mismatched org presence or
|
|
14
|
+
* different orgs never match: two subscriptions (orgs) can share one
|
|
15
|
+
* email, so an org-scoped identity matches only its own org's reports and
|
|
16
|
+
* an org-less legacy identity never claims an org-attributed report via
|
|
17
|
+
* the shared email. A SHARED org still requires the base-identity match
|
|
18
|
+
* below — Anthropic Team seats have per-user pools yet share the org id
|
|
19
|
+
* in report metadata. Only an org-only identity (no base identifiers
|
|
20
|
+
* recovered at all) matches on the org alone. When neither side carries
|
|
21
|
+
* an org, the base fallback applies unchanged (providers without orgs
|
|
22
|
+
* keep their former behavior).
|
|
12
23
|
* - `accountId` ↔ report metadata `accountId`/`account_id` or `limit.scope.accountId`
|
|
13
24
|
* - `email` ↔ report metadata `email`
|
|
14
25
|
* - `projectId` ↔ report metadata `projectId` or `limit.scope.projectId`
|
|
@@ -22,14 +33,23 @@ export function limitMatchesActiveAccount(
|
|
|
22
33
|
if (!identity) return false;
|
|
23
34
|
const metadata = report.metadata ?? {};
|
|
24
35
|
const activeAccountId = normalizeIdentityValue(identity.accountId);
|
|
36
|
+
const activeEmail = normalizeIdentityValue(identity.email);
|
|
37
|
+
const activeProjectId = normalizeIdentityValue(identity.projectId);
|
|
38
|
+
const activeOrgId = normalizeIdentityValue(identity.orgId);
|
|
39
|
+
const reportOrgId = normalizeIdentityValue(metadata.orgId);
|
|
40
|
+
// Org gate (see doc comment above): different/mismatched-presence orgs
|
|
41
|
+
// never match; a shared org falls through to the base checks unless the
|
|
42
|
+
// identity is org-only.
|
|
43
|
+
if (activeOrgId || reportOrgId) {
|
|
44
|
+
if (activeOrgId !== reportOrgId) return false;
|
|
45
|
+
if (!activeAccountId && !activeEmail && !activeProjectId) return true;
|
|
46
|
+
}
|
|
25
47
|
if (activeAccountId) {
|
|
26
48
|
const reportAccountId = normalizeIdentityValue(metadata.accountId) ?? normalizeIdentityValue(metadata.account_id);
|
|
27
49
|
if (reportAccountId === activeAccountId) return true;
|
|
28
50
|
if (normalizeIdentityValue(limit.scope.accountId) === activeAccountId) return true;
|
|
29
51
|
}
|
|
30
|
-
const activeEmail = normalizeIdentityValue(identity.email);
|
|
31
52
|
if (activeEmail && normalizeIdentityValue(metadata.email) === activeEmail) return true;
|
|
32
|
-
const activeProjectId = normalizeIdentityValue(identity.projectId);
|
|
33
53
|
if (activeProjectId) {
|
|
34
54
|
if (normalizeIdentityValue(metadata.projectId) === activeProjectId) return true;
|
|
35
55
|
if (normalizeIdentityValue(limit.scope.projectId) === activeProjectId) return true;
|
|
@@ -22,13 +22,16 @@ function nonEmpty(value: string | undefined): string | undefined {
|
|
|
22
22
|
function oauthLabel(row: StoredAuthCredential): string {
|
|
23
23
|
const credential = row.credential;
|
|
24
24
|
if (credential.type !== "oauth") return `API key #${row.id}`;
|
|
25
|
-
|
|
25
|
+
const base =
|
|
26
26
|
nonEmpty(credential.email) ??
|
|
27
27
|
nonEmpty(credential.accountId) ??
|
|
28
28
|
nonEmpty(credential.projectId) ??
|
|
29
29
|
nonEmpty(credential.enterpriseUrl) ??
|
|
30
|
-
`OAuth credential #${row.id}
|
|
31
|
-
)
|
|
30
|
+
`OAuth credential #${row.id}`;
|
|
31
|
+
// Two subscriptions (orgs) can share one email — the org is the only
|
|
32
|
+
// user-visible way to tell which row a logout will remove.
|
|
33
|
+
const org = nonEmpty(credential.orgName) ?? nonEmpty(credential.orgId);
|
|
34
|
+
return org && org !== base ? `${base} (${org})` : base;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
function oauthDetail(row: StoredAuthCredential, label: string): string {
|
|
@@ -53,6 +56,23 @@ function oauthMatchesActiveIdentity(
|
|
|
53
56
|
): boolean {
|
|
54
57
|
if (!activeIdentity || row.credential.type !== "oauth") return false;
|
|
55
58
|
const credential = row.credential;
|
|
59
|
+
// The org GATES the base identity rather than replacing it: mismatched org
|
|
60
|
+
// presence or different orgs never match — an org-scoped active session
|
|
61
|
+
// must not preselect the bare-email legacy row, and a bare-email active
|
|
62
|
+
// row must not mark org-scoped siblings active via the shared email. A
|
|
63
|
+
// SHARED org still requires the base-identity match below: two Team seats
|
|
64
|
+
// share one orgId yet own distinct rows. Only an org-only active identity
|
|
65
|
+
// (no base identifiers recovered at all) matches on the org alone.
|
|
66
|
+
if (activeIdentity.orgId !== undefined || credential.orgId !== undefined) {
|
|
67
|
+
if (credential.orgId !== activeIdentity.orgId) return false;
|
|
68
|
+
if (
|
|
69
|
+
activeIdentity.accountId === undefined &&
|
|
70
|
+
activeIdentity.email === undefined &&
|
|
71
|
+
activeIdentity.projectId === undefined
|
|
72
|
+
) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
56
76
|
return (
|
|
57
77
|
(activeIdentity.accountId !== undefined && credential.accountId === activeIdentity.accountId) ||
|
|
58
78
|
(activeIdentity.email !== undefined && credential.email === activeIdentity.email) ||
|
|
@@ -25,14 +25,26 @@ function formatUsageAmount(limit: UsageLimit): string {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
function formatUsageReportAccount(report: UsageReport, limit: UsageLimit, index: number): string {
|
|
28
|
+
const metaOrgName = report.metadata?.orgName;
|
|
29
|
+
const metaOrgId = report.metadata?.orgId;
|
|
30
|
+
const org =
|
|
31
|
+
typeof metaOrgName === "string" && metaOrgName
|
|
32
|
+
? metaOrgName
|
|
33
|
+
: typeof metaOrgId === "string" && metaOrgId
|
|
34
|
+
? metaOrgId
|
|
35
|
+
: undefined;
|
|
36
|
+
// Two subscriptions (orgs) can share one email — suffix the org so the rows
|
|
37
|
+
// are tellable apart.
|
|
28
38
|
const email = report.metadata?.email;
|
|
29
|
-
if (typeof email === "string" && email) return email;
|
|
39
|
+
if (typeof email === "string" && email) return org ? `${email} (${org})` : email;
|
|
30
40
|
// Guard metadata values for truthiness before using, then fall back to scope.
|
|
31
41
|
// ?? won't help here: empty string is not null/undefined, so it would suppress
|
|
32
42
|
// a valid scoped fallback (e.g. metadata.accountId="" hides limit.scope.accountId).
|
|
33
43
|
const metaAccountId = report.metadata?.accountId;
|
|
34
44
|
const accountId = typeof metaAccountId === "string" && metaAccountId ? metaAccountId : limit.scope.accountId;
|
|
35
|
-
if (typeof accountId === "string" && accountId)
|
|
45
|
+
if (typeof accountId === "string" && accountId) {
|
|
46
|
+
return org && org !== accountId ? `${accountId} (${org})` : accountId;
|
|
47
|
+
}
|
|
36
48
|
const metaProjectId = report.metadata?.projectId;
|
|
37
49
|
const projectId = typeof metaProjectId === "string" && metaProjectId ? metaProjectId : limit.scope.projectId;
|
|
38
50
|
if (typeof projectId === "string" && projectId) return projectId;
|
|
@@ -159,6 +159,12 @@ export function createWorkerSubprocess<Outbound>(options: {
|
|
|
159
159
|
spawnCommand: WorkerSpawnCommand;
|
|
160
160
|
env: Record<string, string>;
|
|
161
161
|
exitLabel: string;
|
|
162
|
+
/** Start the child as a new process-group/session leader where Bun supports it. */
|
|
163
|
+
detached?: boolean;
|
|
164
|
+
/** Treat exit code 0 as unexpected; eval cells can call process.exit(0). */
|
|
165
|
+
reportCleanExit?: boolean;
|
|
166
|
+
/** Whether an idle worker should stop keeping the parent event loop alive. */
|
|
167
|
+
unref?: boolean;
|
|
162
168
|
}): SpawnedSubprocess<Outbound> {
|
|
163
169
|
const inbound = new Set<(message: Outbound) => void>();
|
|
164
170
|
const errors = new Set<(error: Error) => void>();
|
|
@@ -175,6 +181,7 @@ export function createWorkerSubprocess<Outbound>(options: {
|
|
|
175
181
|
const proc = Bun.spawn({
|
|
176
182
|
cmd: options.spawnCommand.cmd,
|
|
177
183
|
cwd: options.spawnCommand.cwd,
|
|
184
|
+
detached: options.detached,
|
|
178
185
|
env: options.env,
|
|
179
186
|
stdin: "ignore",
|
|
180
187
|
stdout: "ignore",
|
|
@@ -186,7 +193,7 @@ export function createWorkerSubprocess<Outbound>(options: {
|
|
|
186
193
|
},
|
|
187
194
|
onExit(_proc, exitCode, signalCode) {
|
|
188
195
|
startStderrDrain();
|
|
189
|
-
if (exitCode === 0) return;
|
|
196
|
+
if (exitCode === 0 && !options.reportCleanExit) return;
|
|
190
197
|
// Swallow only the expected SIGKILL from `terminate()`; every other
|
|
191
198
|
// signal exit (SIGSEGV from a native fault, OOM SIGKILL, operator
|
|
192
199
|
// `kill -9`) is a real worker death that must fault in-flight
|
|
@@ -206,7 +213,7 @@ export function createWorkerSubprocess<Outbound>(options: {
|
|
|
206
213
|
// Don't keep the parent event loop alive on an idle worker; the dispose
|
|
207
214
|
// path calls `terminate()` explicitly. Bun's test runner starves IPC for
|
|
208
215
|
// unref'd subprocesses, so keep it referenced only under tests.
|
|
209
|
-
if (!isBunTestRuntime()) proc.unref();
|
|
216
|
+
if (!isBunTestRuntime() && options.unref !== false) proc.unref();
|
|
210
217
|
return { proc, inbound, errors, intentionalExit, stderrDrained: stderrDrained.promise };
|
|
211
218
|
}
|
|
212
219
|
|
package/src/task/executor.ts
CHANGED
|
@@ -2325,14 +2325,22 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2325
2325
|
thinkingLevel: resolvedThinkingLevel,
|
|
2326
2326
|
explicitThinkingLevel,
|
|
2327
2327
|
authFallbackUsed,
|
|
2328
|
+
warning: modelResolutionWarning,
|
|
2328
2329
|
} = await awaitAbortable(
|
|
2329
2330
|
resolveModelOverrideWithAuthFallback(
|
|
2330
2331
|
modelPatterns,
|
|
2331
2332
|
options.parentActiveModelPattern,
|
|
2332
2333
|
modelRegistry,
|
|
2333
2334
|
settings,
|
|
2335
|
+
id,
|
|
2334
2336
|
),
|
|
2335
2337
|
);
|
|
2338
|
+
if (modelResolutionWarning) {
|
|
2339
|
+
logger.warn("Subagent model resolution warning", {
|
|
2340
|
+
warning: modelResolutionWarning,
|
|
2341
|
+
requested: modelPatterns,
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2336
2344
|
if (authFallbackUsed && model) {
|
|
2337
2345
|
logger.warn("Subagent model has no working credentials; falling back to parent session model", {
|
|
2338
2346
|
requested: modelPatterns,
|
package/src/task/render.test.ts
CHANGED
|
@@ -184,6 +184,42 @@ describe("task live progress rendering", () => {
|
|
|
184
184
|
expect(collapsedText).not.toContain("line 1");
|
|
185
185
|
expect(collapsedText).not.toContain("raw output:");
|
|
186
186
|
});
|
|
187
|
+
it("sanitizes control sequences from expanded subagent recent output", () => {
|
|
188
|
+
setViewportRows(40);
|
|
189
|
+
const progress = makeProgress(["safe after \x1b[2Kclear", "raw\rprompt"]);
|
|
190
|
+
|
|
191
|
+
const text = renderProgressText(progress, true, uiTheme);
|
|
192
|
+
|
|
193
|
+
expect(text).toContain("safe after clear");
|
|
194
|
+
expect(text).toContain("rawprompt");
|
|
195
|
+
expect(text).not.toContain("\x1b[2K");
|
|
196
|
+
expect(text).not.toContain("\r");
|
|
197
|
+
});
|
|
198
|
+
it("sanitizes control sequences from finalized subagent results", () => {
|
|
199
|
+
const output = JSON.stringify({ "\x1b[2Kkey": "safe value" });
|
|
200
|
+
const details: TaskToolDetails = {
|
|
201
|
+
projectAgentsDir: null,
|
|
202
|
+
results: [
|
|
203
|
+
makeSingleResult(0, {
|
|
204
|
+
id: "Final\x1b[2KAgent",
|
|
205
|
+
description: "description\rtext",
|
|
206
|
+
aborted: true,
|
|
207
|
+
abortReason: "aborted \x1b[2Kreason",
|
|
208
|
+
output,
|
|
209
|
+
}),
|
|
210
|
+
],
|
|
211
|
+
totalDurationMs: 1,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const text = renderResultText(details, true, uiTheme);
|
|
215
|
+
|
|
216
|
+
expect(text).toContain("FinalAgent");
|
|
217
|
+
expect(text).toContain("descriptiontext");
|
|
218
|
+
expect(text).toContain("aborted reason");
|
|
219
|
+
expect(text).toContain("key");
|
|
220
|
+
expect(text).not.toContain("\x1b[2K");
|
|
221
|
+
expect(text).not.toContain("\r");
|
|
222
|
+
});
|
|
187
223
|
|
|
188
224
|
it("caps collapsed nested task progress at four rows plus an elision line", () => {
|
|
189
225
|
setViewportRows(40);
|
package/src/task/render.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
9
9
|
import { Container, Markdown, Text } from "@oh-my-pi/pi-tui";
|
|
10
|
-
import { formatNumber } from "@oh-my-pi/pi-utils";
|
|
10
|
+
import { formatNumber, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
11
11
|
import { settings } from "../config/settings";
|
|
12
12
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
13
13
|
import { formatContextUsage } from "../modes/components/status-line/context-thresholds";
|
|
@@ -249,11 +249,11 @@ function getRenderYieldLabels(type: RenderYieldItem["type"]): string[] {
|
|
|
249
249
|
function formatYieldPreview(item: RenderYieldItem): string {
|
|
250
250
|
if (item.useLastTurn === true && item.data === undefined) return "last assistant turn";
|
|
251
251
|
if (item.data === undefined) return "last assistant turn";
|
|
252
|
-
if (typeof item.data === "string") return previewLine(replaceTabs(item.data), 70);
|
|
252
|
+
if (typeof item.data === "string") return previewLine(replaceTabs(sanitizeText(item.data)), 70);
|
|
253
253
|
try {
|
|
254
|
-
return previewLine(replaceTabs(JSON.stringify(item.data) ?? "null"), 70);
|
|
254
|
+
return previewLine(replaceTabs(sanitizeText(JSON.stringify(item.data) ?? "null")), 70);
|
|
255
255
|
} catch {
|
|
256
|
-
return previewLine(replaceTabs(String(item.data)), 70);
|
|
256
|
+
return previewLine(replaceTabs(sanitizeText(String(item.data))), 70);
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
259
|
|
|
@@ -281,7 +281,7 @@ function renderTypedYieldSections(value: unknown, continuePrefix: string, expand
|
|
|
281
281
|
function formatJsonScalar(value: unknown, _theme: Theme): string {
|
|
282
282
|
if (value === null) return "null";
|
|
283
283
|
if (typeof value === "string") {
|
|
284
|
-
const trimmed = truncateToWidth(value, 70);
|
|
284
|
+
const trimmed = truncateToWidth(sanitizeText(value), 70);
|
|
285
285
|
return `"${trimmed}"`;
|
|
286
286
|
}
|
|
287
287
|
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
@@ -291,8 +291,9 @@ function formatJsonScalar(value: unknown, _theme: Theme): string {
|
|
|
291
291
|
export function formatTaskId(id: string): string {
|
|
292
292
|
// Ids are name-based (e.g. "Anna", "Anna-2"); a "." separates nesting levels
|
|
293
293
|
// (e.g. "Anna.Bob"). Render the hierarchy with a ">" breadcrumb.
|
|
294
|
-
const
|
|
295
|
-
|
|
294
|
+
const sanitizedId = sanitizeText(id);
|
|
295
|
+
const segments = sanitizedId.split(".");
|
|
296
|
+
return segments.length < 2 ? sanitizedId : segments.join(">");
|
|
296
297
|
}
|
|
297
298
|
|
|
298
299
|
const MISSING_YIELD_WARNING_PREFIX = "SYSTEM WARNING: Subagent exited without calling yield tool";
|
|
@@ -347,13 +348,13 @@ function renderJsonTreeLines(
|
|
|
347
348
|
const scalar = formatJsonScalar(val, theme);
|
|
348
349
|
|
|
349
350
|
if (scalar) {
|
|
350
|
-
const label = key ? theme.fg("muted", key) : theme.fg("muted", "value");
|
|
351
|
+
const label = key ? theme.fg("muted", sanitizeText(key)) : theme.fg("muted", "value");
|
|
351
352
|
pushLine(`${prefix}${iconScalar} ${label}: ${theme.fg("dim", scalar)}`);
|
|
352
353
|
return;
|
|
353
354
|
}
|
|
354
355
|
|
|
355
356
|
if (Array.isArray(val)) {
|
|
356
|
-
const header = key ? theme.fg("muted", key) : theme.fg("muted", "array");
|
|
357
|
+
const header = key ? theme.fg("muted", sanitizeText(key)) : theme.fg("muted", "array");
|
|
357
358
|
pushLine(`${prefix}${iconArray} ${header}`);
|
|
358
359
|
if (val.length === 0) {
|
|
359
360
|
pushLine(
|
|
@@ -385,7 +386,7 @@ function renderJsonTreeLines(
|
|
|
385
386
|
}
|
|
386
387
|
|
|
387
388
|
if (val && typeof val === "object") {
|
|
388
|
-
const header = key ? theme.fg("muted", key) : theme.fg("muted", "object");
|
|
389
|
+
const header = key ? theme.fg("muted", sanitizeText(key)) : theme.fg("muted", "object");
|
|
389
390
|
pushLine(`${prefix}${iconObject} ${header}`);
|
|
390
391
|
const entries = Object.entries(val as Record<string, unknown>);
|
|
391
392
|
if (entries.length === 0) {
|
|
@@ -418,8 +419,8 @@ function renderJsonTreeLines(
|
|
|
418
419
|
return;
|
|
419
420
|
}
|
|
420
421
|
|
|
421
|
-
const label = key ? theme.fg("muted", key) : theme.fg("muted", "value");
|
|
422
|
-
pushLine(`${prefix}${iconScalar} ${label}: ${theme.fg("dim", String(val))}`);
|
|
422
|
+
const label = key ? theme.fg("muted", sanitizeText(key)) : theme.fg("muted", "value");
|
|
423
|
+
pushLine(`${prefix}${iconScalar} ${label}: ${theme.fg("dim", sanitizeText(String(val)))}`);
|
|
423
424
|
};
|
|
424
425
|
|
|
425
426
|
const renderRoot = (val: unknown) => {
|
|
@@ -466,7 +467,7 @@ function stripRecentOutputNoticeLine(text: string): string {
|
|
|
466
467
|
}
|
|
467
468
|
|
|
468
469
|
function sanitizeRecentOutput(output: string): string {
|
|
469
|
-
let text = output.trimEnd();
|
|
470
|
+
let text = sanitizeText(output).trimEnd();
|
|
470
471
|
while (text) {
|
|
471
472
|
const withoutArtifactNotice = stripRawOutputArtifactNotice(text).text;
|
|
472
473
|
if (withoutArtifactNotice !== text) {
|
|
@@ -498,7 +499,8 @@ function renderOutputSection(
|
|
|
498
499
|
warning?: string,
|
|
499
500
|
): string[] {
|
|
500
501
|
const lines: string[] = [];
|
|
501
|
-
const
|
|
502
|
+
const sanitizedOutput = sanitizeText(output);
|
|
503
|
+
const trimmedOutput = sanitizedOutput.trimEnd();
|
|
502
504
|
if (!trimmedOutput && !warning) return lines;
|
|
503
505
|
|
|
504
506
|
if (warning) {
|
|
@@ -506,7 +508,7 @@ function renderOutputSection(
|
|
|
506
508
|
lines.push(
|
|
507
509
|
`${continuePrefix} ${theme.fg("warning", theme.status.warning)} ${theme.fg(
|
|
508
510
|
"dim",
|
|
509
|
-
truncateToWidth(warning, 80),
|
|
511
|
+
truncateToWidth(sanitizeText(warning), 80),
|
|
510
512
|
)}`,
|
|
511
513
|
);
|
|
512
514
|
|
|
@@ -538,7 +540,7 @@ function renderOutputSection(
|
|
|
538
540
|
}
|
|
539
541
|
}
|
|
540
542
|
|
|
541
|
-
const outputLines =
|
|
543
|
+
const outputLines = trimmedOutput.split("\n");
|
|
542
544
|
const previewCount = expanded ? maxExpanded : maxCollapsed;
|
|
543
545
|
for (const line of outputLines.slice(0, previewCount)) {
|
|
544
546
|
lines.push(`${continuePrefix} ${theme.fg("dim", truncateToWidth(replaceTabs(line), 70))}`);
|
|
@@ -582,7 +584,7 @@ function renderOutputSection(
|
|
|
582
584
|
|
|
583
585
|
lines.push(`${continuePrefix}${theme.fg("dim", "Output")}`);
|
|
584
586
|
|
|
585
|
-
const outputLines =
|
|
587
|
+
const outputLines = trimmedOutput.split("\n");
|
|
586
588
|
const previewCount = expanded ? maxExpanded : maxCollapsed;
|
|
587
589
|
for (const line of outputLines.slice(0, previewCount)) {
|
|
588
590
|
lines.push(`${continuePrefix} ${theme.fg("dim", truncateToWidth(replaceTabs(line), 70))}`);
|
|
@@ -603,7 +605,7 @@ function renderTaskSection(
|
|
|
603
605
|
maxExpanded = 20,
|
|
604
606
|
): string[] {
|
|
605
607
|
const lines: string[] = [];
|
|
606
|
-
const trimmed = task.trim();
|
|
608
|
+
const trimmed = sanitizeText(task).trim();
|
|
607
609
|
if (!expanded || !trimmed) return lines;
|
|
608
610
|
|
|
609
611
|
lines.push(`${continuePrefix}${theme.fg("dim", "Task")}`);
|
|
@@ -624,10 +626,11 @@ function formatScalarInline(value: unknown, maxLen: number, _theme: Theme): stri
|
|
|
624
626
|
if (typeof value === "boolean") return String(value);
|
|
625
627
|
if (typeof value === "number") return String(value);
|
|
626
628
|
if (typeof value === "string") {
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
+
const sanitizedValue = sanitizeText(value);
|
|
630
|
+
const firstLine = sanitizedValue.split("\n")[0].trim();
|
|
631
|
+
if (firstLine.length === 0) return `"" (${sanitizedValue.split("\n").length} lines)`;
|
|
629
632
|
const preview = truncateToWidth(firstLine, maxLen);
|
|
630
|
-
if (
|
|
633
|
+
if (sanitizedValue.includes("\n")) return `"${preview}…" (${sanitizedValue.split("\n").length} lines)`;
|
|
631
634
|
return `"${preview}"`;
|
|
632
635
|
}
|
|
633
636
|
if (Array.isArray(value)) return `[${value.length} items]`;
|
|
@@ -635,7 +638,7 @@ function formatScalarInline(value: unknown, maxLen: number, _theme: Theme): stri
|
|
|
635
638
|
const keys = Object.keys(value);
|
|
636
639
|
return `{${keys.length} keys}`;
|
|
637
640
|
}
|
|
638
|
-
return String(value);
|
|
641
|
+
return sanitizeText(String(value));
|
|
639
642
|
}
|
|
640
643
|
|
|
641
644
|
function formatOutputInline(data: unknown, theme: Theme, maxWidth = 80): string {
|
|
@@ -662,7 +665,7 @@ function formatOutputInline(data: unknown, theme: Theme, maxWidth = 80): string
|
|
|
662
665
|
|
|
663
666
|
for (const [key, value] of entries) {
|
|
664
667
|
const valueStr = formatScalarInline(value, 24, theme);
|
|
665
|
-
const pairStr = `${key}=${valueStr}`;
|
|
668
|
+
const pairStr = `${sanitizeText(key)}=${valueStr}`;
|
|
666
669
|
const addLen = pairs.length > 0 ? pairStr.length + 2 : pairStr.length; // +2 for ", "
|
|
667
670
|
|
|
668
671
|
if (totalLen + addLen > maxWidth && pairs.length > 0) {
|
|
@@ -683,7 +686,7 @@ function formatOutputInline(data: unknown, theme: Theme, maxWidth = 80): string
|
|
|
683
686
|
*/
|
|
684
687
|
function taskFirstLine(task: unknown): string {
|
|
685
688
|
if (typeof task !== "string") return "";
|
|
686
|
-
const trimmed = task.trim();
|
|
689
|
+
const trimmed = sanitizeText(task).trim();
|
|
687
690
|
const newline = trimmed.indexOf("\n");
|
|
688
691
|
return newline === -1 ? trimmed : trimmed.slice(0, newline);
|
|
689
692
|
}
|
|
@@ -791,7 +794,9 @@ function createAssignmentSectionRenderer(
|
|
|
791
794
|
// `renderResult` receives the raw tool args (unlike `renderCall`, which is
|
|
792
795
|
// fed through `repairTaskParams`), so undo any per-field double-encoding
|
|
793
796
|
// here too. The repair is idempotent on already-clean text.
|
|
794
|
-
const assignment =
|
|
797
|
+
const assignment = sanitizeText(
|
|
798
|
+
repairDoubleEncodedJsonString(typeof args?.task === "string" ? args.task : ""),
|
|
799
|
+
).trim();
|
|
795
800
|
if (!assignment) return undefined;
|
|
796
801
|
return createMarkdownSectionRenderer(assignment, theme);
|
|
797
802
|
}
|
|
@@ -805,7 +810,9 @@ function createContextSectionRenderer(
|
|
|
805
810
|
args: Partial<TaskParams> | undefined,
|
|
806
811
|
theme: Theme,
|
|
807
812
|
): AssignmentSectionRenderer | undefined {
|
|
808
|
-
const context =
|
|
813
|
+
const context = sanitizeText(
|
|
814
|
+
repairDoubleEncodedJsonString(typeof args?.context === "string" ? args.context : ""),
|
|
815
|
+
).trim();
|
|
809
816
|
if (!context) return undefined;
|
|
810
817
|
return createMarkdownSectionRenderer(context, theme);
|
|
811
818
|
}
|
|
@@ -894,7 +901,7 @@ function renderAgentProgress(
|
|
|
894
901
|
|
|
895
902
|
// Main status line: id: description [status] · stats · ⟨agent⟩
|
|
896
903
|
const trimmedDescription = progress.description?.trim();
|
|
897
|
-
const description = trimmedDescription ? previewLine(trimmedDescription, 64) : undefined;
|
|
904
|
+
const description = trimmedDescription ? previewLine(sanitizeText(trimmedDescription), 64) : undefined;
|
|
898
905
|
const displayId = formatTaskId(progress.id);
|
|
899
906
|
const titlePart = description ? `${theme.bold(displayId)}: ${description}` : displayId;
|
|
900
907
|
const indent = prefix ? `${prefix} ` : "";
|
|
@@ -936,7 +943,7 @@ function renderAgentProgress(
|
|
|
936
943
|
const showBadge = settings.get("task.showResolvedModelBadge");
|
|
937
944
|
if (progress.status === "running") {
|
|
938
945
|
if (!description) {
|
|
939
|
-
const taskPreview = previewLine(progress.assignment ?? progress.task, 40);
|
|
946
|
+
const taskPreview = previewLine(sanitizeText(progress.assignment ?? progress.task), 40);
|
|
940
947
|
statusLine += ` ${theme.fg("muted", taskPreview)}`;
|
|
941
948
|
}
|
|
942
949
|
statusLine = appendAgentStats(statusLine, { ...progress, showResolvedModelBadge: showBadge }, theme);
|
|
@@ -951,10 +958,10 @@ function renderAgentProgress(
|
|
|
951
958
|
// Current tool (if running) or most recent completed tool
|
|
952
959
|
if (progress.status === "running") {
|
|
953
960
|
if (progress.currentTool) {
|
|
954
|
-
let toolLine = `${continuePrefix}${theme.tree.hook} ${theme.fg("muted", progress.currentTool)}`;
|
|
961
|
+
let toolLine = `${continuePrefix}${theme.tree.hook} ${theme.fg("muted", sanitizeText(progress.currentTool))}`;
|
|
955
962
|
const toolDetail = progress.lastIntent ?? progress.currentToolArgs;
|
|
956
963
|
if (toolDetail) {
|
|
957
|
-
toolLine += `: ${theme.fg("dim", previewLine(toolDetail, 40))}`;
|
|
964
|
+
toolLine += `: ${theme.fg("dim", previewLine(sanitizeText(toolDetail), 40))}`;
|
|
958
965
|
}
|
|
959
966
|
if (progress.currentToolStartMs) {
|
|
960
967
|
const elapsed = Date.now() - progress.currentToolStartMs;
|
|
@@ -966,10 +973,10 @@ function renderAgentProgress(
|
|
|
966
973
|
} else if (progress.recentTools.length > 0) {
|
|
967
974
|
// Show most recent completed tool when idle between tools
|
|
968
975
|
const recent = progress.recentTools[0];
|
|
969
|
-
let toolLine = `${continuePrefix}${theme.tree.hook} ${theme.fg("dim", recent.tool)}`;
|
|
976
|
+
let toolLine = `${continuePrefix}${theme.tree.hook} ${theme.fg("dim", sanitizeText(recent.tool))}`;
|
|
970
977
|
const toolDetail = progress.lastIntent ?? recent.args;
|
|
971
978
|
if (toolDetail) {
|
|
972
|
-
toolLine += `: ${theme.fg("dim", previewLine(toolDetail, 40))}`;
|
|
979
|
+
toolLine += `: ${theme.fg("dim", previewLine(sanitizeText(toolDetail), 40))}`;
|
|
973
980
|
}
|
|
974
981
|
lines.push(toolLine);
|
|
975
982
|
}
|
|
@@ -983,12 +990,12 @@ function renderAgentProgress(
|
|
|
983
990
|
const waitLabel = remainingMs > 0 ? `in ${formatDuration(remainingMs)}` : "now";
|
|
984
991
|
const summary =
|
|
985
992
|
`retrying ${progress.retryState.attempt}/${progress.retryState.maxAttempts} ${waitLabel}: ` +
|
|
986
|
-
previewLine(progress.retryState.errorMessage, 60);
|
|
993
|
+
previewLine(sanitizeText(progress.retryState.errorMessage), 60);
|
|
987
994
|
lines.push(`${continuePrefix}${theme.tree.hook} ${theme.fg("warning", summary)}`);
|
|
988
995
|
} else if (progress.retryFailure && progress.status !== "running") {
|
|
989
996
|
const summary = `auto-retry gave up after ${progress.retryFailure.attempt} attempt${
|
|
990
997
|
progress.retryFailure.attempt === 1 ? "" : "s"
|
|
991
|
-
}: ${previewLine(progress.retryFailure.errorMessage, 80)}`;
|
|
998
|
+
}: ${previewLine(sanitizeText(progress.retryFailure.errorMessage), 80)}`;
|
|
992
999
|
lines.push(`${continuePrefix}${theme.tree.hook} ${theme.fg("error", summary)}`);
|
|
993
1000
|
}
|
|
994
1001
|
|
|
@@ -1134,13 +1141,13 @@ function renderReviewResult(
|
|
|
1134
1141
|
if (summary.explanation) {
|
|
1135
1142
|
if (expanded) {
|
|
1136
1143
|
lines.push(`${continuePrefix}${theme.fg("dim", "Summary")}`);
|
|
1137
|
-
const explanationLines = summary.explanation.split("\n");
|
|
1144
|
+
const explanationLines = sanitizeText(summary.explanation).split("\n");
|
|
1138
1145
|
for (const line of explanationLines) {
|
|
1139
1146
|
lines.push(`${continuePrefix} ${theme.fg("dim", replaceTabs(line))}`);
|
|
1140
1147
|
}
|
|
1141
1148
|
} else {
|
|
1142
1149
|
// Preview: first sentence or ~100 chars (flatten tabs/newlines first)
|
|
1143
|
-
const flat = replaceTabs(summary.explanation).replace(/[\r\n]+/g, " ");
|
|
1150
|
+
const flat = replaceTabs(sanitizeText(summary.explanation)).replace(/[\r\n]+/g, " ");
|
|
1144
1151
|
const firstSentence = flat.split(/[.!?]/)[0].trim();
|
|
1145
1152
|
const preview = truncateToWidth(`${firstSentence}.`, 100);
|
|
1146
1153
|
lines.push(`${continuePrefix}${theme.fg("dim", preview)}`);
|
|
@@ -1181,9 +1188,9 @@ function renderFindings(
|
|
|
1181
1188
|
const findingContinue = isLastFinding ? " " : `${theme.tree.vertical} `;
|
|
1182
1189
|
|
|
1183
1190
|
const { color } = getPriorityInfo(finding.priority);
|
|
1184
|
-
const rawTitle = finding.title?.replace(/^\[P\d\]\s*/, "") ?? "Untitled";
|
|
1191
|
+
const rawTitle = sanitizeText(finding.title?.replace(/^\[P\d\]\s*/, "") ?? "Untitled");
|
|
1185
1192
|
const titleText = replaceTabs(rawTitle).replace(/[\r\n]+/g, " ");
|
|
1186
|
-
const loc = `${path.basename(finding.file_path || "<unknown>")}:${finding.line_start}`;
|
|
1193
|
+
const loc = `${path.basename(sanitizeText(finding.file_path || "<unknown>"))}:${finding.line_start}`;
|
|
1187
1194
|
|
|
1188
1195
|
lines.push(
|
|
1189
1196
|
`${continuePrefix}${findingPrefix} ${theme.fg(color, `[${finding.priority}]`)} ${titleText} ${theme.fg("dim", loc)}`,
|
|
@@ -1192,7 +1199,7 @@ function renderFindings(
|
|
|
1192
1199
|
// Show body when expanded
|
|
1193
1200
|
if (expanded && finding.body) {
|
|
1194
1201
|
// Wrap body text
|
|
1195
|
-
const bodyLines = finding.body.split("\n");
|
|
1202
|
+
const bodyLines = sanitizeText(finding.body).split("\n");
|
|
1196
1203
|
for (const bodyLine of bodyLines) {
|
|
1197
1204
|
lines.push(`${continuePrefix}${findingContinue}${theme.fg("dim", replaceTabs(bodyLine))}`);
|
|
1198
1205
|
}
|
|
@@ -1244,7 +1251,7 @@ function renderAgentResult(
|
|
|
1244
1251
|
: "failed";
|
|
1245
1252
|
|
|
1246
1253
|
// Main status line: id: description [status] · stats · ⟨agent⟩
|
|
1247
|
-
const trimmedDescription = result.description
|
|
1254
|
+
const trimmedDescription = result.description ? sanitizeText(result.description).trim() : undefined;
|
|
1248
1255
|
const description = trimmedDescription ? previewLine(trimmedDescription, 64) : undefined;
|
|
1249
1256
|
const displayId = formatTaskId(result.id);
|
|
1250
1257
|
const titlePart = description ? `${theme.bold(displayId)}: ${description}` : displayId;
|
|
@@ -1278,7 +1285,10 @@ function renderAgentResult(
|
|
|
1278
1285
|
|
|
1279
1286
|
if (aborted && result.abortReason) {
|
|
1280
1287
|
lines.push(
|
|
1281
|
-
`${continuePrefix}${theme.fg("error", theme.status.aborted)} ${theme.fg(
|
|
1288
|
+
`${continuePrefix}${theme.fg("error", theme.status.aborted)} ${theme.fg(
|
|
1289
|
+
"dim",
|
|
1290
|
+
previewLine(sanitizeText(result.abortReason), 80),
|
|
1291
|
+
)}`,
|
|
1282
1292
|
);
|
|
1283
1293
|
}
|
|
1284
1294
|
// Check for review result, preferring incremental yield sections and falling
|
|
@@ -1382,7 +1392,7 @@ function renderAgentResult(
|
|
|
1382
1392
|
lines.push(
|
|
1383
1393
|
`${continuePrefix}${theme.fg("warning", theme.status.warning)} ${theme.fg(
|
|
1384
1394
|
"dim",
|
|
1385
|
-
truncateToWidth(missingCompleteWarning, 80),
|
|
1395
|
+
truncateToWidth(sanitizeText(missingCompleteWarning), 80),
|
|
1386
1396
|
)}`,
|
|
1387
1397
|
);
|
|
1388
1398
|
}
|
|
@@ -1406,7 +1416,9 @@ function renderAgentResult(
|
|
|
1406
1416
|
|
|
1407
1417
|
// Error message
|
|
1408
1418
|
if (result.error && (!success || mergeFailed) && (!aborted || result.error !== result.abortReason)) {
|
|
1409
|
-
lines.push(
|
|
1419
|
+
lines.push(
|
|
1420
|
+
`${continuePrefix}${theme.fg(mergeFailed ? "warning" : "error", previewLine(sanitizeText(result.error), 70))}`,
|
|
1421
|
+
);
|
|
1410
1422
|
}
|
|
1411
1423
|
|
|
1412
1424
|
return lines;
|
|
@@ -27,6 +27,7 @@ export interface InternalUrlExpansionOptions {
|
|
|
27
27
|
noEscape?: boolean;
|
|
28
28
|
internalRouter?: InternalUrlResolver;
|
|
29
29
|
localOptions?: LocalProtocolOptions;
|
|
30
|
+
cwd?: string;
|
|
30
31
|
ensureLocalParentDirs?: boolean;
|
|
31
32
|
}
|
|
32
33
|
|
|
@@ -175,6 +176,7 @@ async function resolveInternalUrlToPath(
|
|
|
175
176
|
internalRouter?: InternalUrlResolver,
|
|
176
177
|
localOptions?: LocalProtocolOptions,
|
|
177
178
|
ensureLocalParentDirs?: boolean,
|
|
179
|
+
cwd?: string,
|
|
178
180
|
): Promise<string> {
|
|
179
181
|
const url = normalizeLocalScheme(rawUrl);
|
|
180
182
|
const scheme = extractScheme(url);
|
|
@@ -208,7 +210,7 @@ async function resolveInternalUrlToPath(
|
|
|
208
210
|
|
|
209
211
|
let resource: InternalResource;
|
|
210
212
|
try {
|
|
211
|
-
resource = await internalRouter.resolve(url, { pathOnly: true });
|
|
213
|
+
resource = await internalRouter.resolve(url, { cwd, pathOnly: true });
|
|
212
214
|
} catch (error) {
|
|
213
215
|
const message = error instanceof Error ? error.message : String(error);
|
|
214
216
|
throw new ToolError(`Failed to resolve ${scheme}:// URL in bash command: ${url}\n${message}`);
|
|
@@ -268,6 +270,7 @@ export async function expandInternalUrls(command: string, options: InternalUrlEx
|
|
|
268
270
|
options.internalRouter,
|
|
269
271
|
options.localOptions,
|
|
270
272
|
options.ensureLocalParentDirs,
|
|
273
|
+
options.cwd,
|
|
271
274
|
);
|
|
272
275
|
} catch {
|
|
273
276
|
continue;
|
package/src/tools/bash.ts
CHANGED
|
@@ -751,6 +751,7 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
751
751
|
const internalUrlOptions: InternalUrlExpansionOptions = {
|
|
752
752
|
skills: this.session.skills ?? [],
|
|
753
753
|
internalRouter: InternalUrlRouter.instance(),
|
|
754
|
+
cwd: this.session.cwd,
|
|
754
755
|
localOptions: {
|
|
755
756
|
getArtifactsDir: this.session.getArtifactsDir,
|
|
756
757
|
getSessionId: this.session.getSessionId,
|