gsd-pi 2.70.1-dev.3e19108 → 2.70.1-dev.7d1d9d3
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/dist/resources/extensions/claude-code-cli/stream-adapter.js +127 -30
- package/dist/resources/extensions/get-secrets-from-user.js +17 -1
- package/dist/resources/extensions/gsd/custom-workflow-engine.js +16 -12
- package/dist/resources/extensions/gsd/file-lock.js +60 -0
- package/dist/resources/extensions/gsd/state.js +234 -332
- package/dist/resources/extensions/gsd/workflow-events.js +25 -13
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +15 -15
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +15 -15
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +256 -1
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts +19 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js +50 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +117 -9
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +6 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +58 -2
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.js.map +1 -1
- package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +317 -1
- package/packages/pi-coding-agent/src/core/extensions/types.ts +2 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts +58 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/extension-input.ts +2 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +128 -15
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +66 -2
- package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +1 -1
- package/packages/pi-coding-agent/src/modes/rpc/rpc-types.ts +1 -0
- package/packages/pi-tui/dist/components/__tests__/input.test.js +9 -0
- package/packages/pi-tui/dist/components/__tests__/input.test.js.map +1 -1
- package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.d.ts +2 -0
- package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.d.ts.map +1 -0
- package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.js +66 -0
- package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.js.map +1 -0
- package/packages/pi-tui/dist/components/input.d.ts +2 -0
- package/packages/pi-tui/dist/components/input.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/input.js +7 -4
- package/packages/pi-tui/dist/components/input.js.map +1 -1
- package/packages/pi-tui/dist/components/markdown.d.ts +3 -0
- package/packages/pi-tui/dist/components/markdown.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/markdown.js +17 -1
- package/packages/pi-tui/dist/components/markdown.js.map +1 -1
- package/packages/pi-tui/src/components/__tests__/input.test.ts +11 -0
- package/packages/pi-tui/src/components/__tests__/markdown-maxlines.test.ts +75 -0
- package/packages/pi-tui/src/components/input.ts +7 -4
- package/packages/pi-tui/src/components/markdown.ts +22 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +164 -31
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +112 -0
- package/src/resources/extensions/get-secrets-from-user.ts +24 -1
- package/src/resources/extensions/gsd/custom-workflow-engine.ts +19 -14
- package/src/resources/extensions/gsd/file-lock.ts +59 -0
- package/src/resources/extensions/gsd/state.ts +274 -344
- package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +436 -0
- package/src/resources/extensions/gsd/tests/file-lock.test.ts +103 -0
- package/src/resources/extensions/gsd/tests/secure-env-collect.test.ts +45 -0
- package/src/resources/extensions/gsd/workflow-events.ts +34 -25
- /package/dist/web/standalone/.next/static/{cHCEWiRJM5bXJa9HkP1QU → 52NuiWbmUzXpzxaTEDopT}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{cHCEWiRJM5bXJa9HkP1QU → 52NuiWbmUzXpzxaTEDopT}/_ssgManifest.js +0 -0
|
@@ -60,6 +60,8 @@ interface SdkElicitationFieldSchema {
|
|
|
60
60
|
type?: string;
|
|
61
61
|
title?: string;
|
|
62
62
|
description?: string;
|
|
63
|
+
format?: string;
|
|
64
|
+
writeOnly?: boolean;
|
|
63
65
|
oneOf?: SdkElicitationRequestOption[];
|
|
64
66
|
items?: {
|
|
65
67
|
anyOf?: SdkElicitationRequestOption[];
|
|
@@ -73,6 +75,7 @@ interface SdkElicitationRequest {
|
|
|
73
75
|
requestedSchema?: {
|
|
74
76
|
type?: string;
|
|
75
77
|
properties?: Record<string, SdkElicitationFieldSchema>;
|
|
78
|
+
required?: string[];
|
|
76
79
|
};
|
|
77
80
|
}
|
|
78
81
|
|
|
@@ -85,7 +88,16 @@ interface ParsedElicitationQuestion extends Question {
|
|
|
85
88
|
noteFieldId?: string;
|
|
86
89
|
}
|
|
87
90
|
|
|
91
|
+
interface ParsedTextInputField {
|
|
92
|
+
id: string;
|
|
93
|
+
title: string;
|
|
94
|
+
description: string;
|
|
95
|
+
required: boolean;
|
|
96
|
+
secure: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
88
99
|
const OTHER_OPTION_LABEL = "None of the above";
|
|
100
|
+
const SENSITIVE_FIELD_PATTERN = /(password|passphrase|secret|token|api[_\s-]*key|private[_\s-]*key|credential)/i;
|
|
89
101
|
|
|
90
102
|
// ---------------------------------------------------------------------------
|
|
91
103
|
// Stream factory
|
|
@@ -274,6 +286,67 @@ export function parseAskUserQuestionsElicitation(
|
|
|
274
286
|
return questions.length > 0 ? questions : null;
|
|
275
287
|
}
|
|
276
288
|
|
|
289
|
+
function isSecureElicitationField(
|
|
290
|
+
requestMessage: string,
|
|
291
|
+
fieldId: string,
|
|
292
|
+
field: SdkElicitationFieldSchema,
|
|
293
|
+
): boolean {
|
|
294
|
+
if (field.format === "password") return true;
|
|
295
|
+
if (field.writeOnly === true) return true;
|
|
296
|
+
|
|
297
|
+
const rawField = field as Record<string, unknown>;
|
|
298
|
+
if (rawField.sensitive === true || rawField["x-sensitive"] === true) return true;
|
|
299
|
+
|
|
300
|
+
const haystack = [
|
|
301
|
+
requestMessage,
|
|
302
|
+
fieldId.replace(/[_-]+/g, " "),
|
|
303
|
+
typeof field.title === "string" ? field.title : "",
|
|
304
|
+
typeof field.description === "string" ? field.description : "",
|
|
305
|
+
]
|
|
306
|
+
.join(" ")
|
|
307
|
+
.toLowerCase();
|
|
308
|
+
|
|
309
|
+
return SENSITIVE_FIELD_PATTERN.test(haystack);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function parseTextInputElicitation(
|
|
313
|
+
request: Pick<SdkElicitationRequest, "message" | "mode" | "requestedSchema">,
|
|
314
|
+
): ParsedTextInputField[] | null {
|
|
315
|
+
if (request.mode && request.mode !== "form") return null;
|
|
316
|
+
const schema = request.requestedSchema as
|
|
317
|
+
| ({ properties?: Record<string, SdkElicitationFieldSchema>; keys?: Record<string, SdkElicitationFieldSchema> } & Record<string, unknown>)
|
|
318
|
+
| undefined;
|
|
319
|
+
const fieldsSource = schema?.properties && typeof schema.properties === "object"
|
|
320
|
+
? schema.properties
|
|
321
|
+
: schema?.keys && typeof schema.keys === "object"
|
|
322
|
+
? schema.keys
|
|
323
|
+
: undefined;
|
|
324
|
+
if (!fieldsSource) return null;
|
|
325
|
+
|
|
326
|
+
const requiredSet = new Set(
|
|
327
|
+
Array.isArray(request.requestedSchema?.required)
|
|
328
|
+
? request.requestedSchema.required.filter((value): value is string => typeof value === "string")
|
|
329
|
+
: [],
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
const fields: ParsedTextInputField[] = [];
|
|
333
|
+
for (const [fieldId, field] of Object.entries(fieldsSource)) {
|
|
334
|
+
if (!field || typeof field !== "object") continue;
|
|
335
|
+
if (field.type !== "string") continue;
|
|
336
|
+
if (Array.isArray(field.oneOf) && field.oneOf.length > 0) continue;
|
|
337
|
+
|
|
338
|
+
fields.push({
|
|
339
|
+
id: fieldId,
|
|
340
|
+
title: typeof field.title === "string" && field.title.length > 0 ? field.title : fieldId,
|
|
341
|
+
description: typeof field.description === "string" ? field.description : "",
|
|
342
|
+
required: requiredSet.has(fieldId),
|
|
343
|
+
secure: isSecureElicitationField(request.message, fieldId, field),
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return fields.length > 0 ? fields : null;
|
|
348
|
+
}
|
|
349
|
+
|
|
277
350
|
export function roundResultToElicitationContent(
|
|
278
351
|
questions: ParsedElicitationQuestion[],
|
|
279
352
|
result: RoundResult,
|
|
@@ -355,6 +428,52 @@ async function promptElicitationWithDialogs(
|
|
|
355
428
|
return { action: "accept", content };
|
|
356
429
|
}
|
|
357
430
|
|
|
431
|
+
function buildTextInputPromptTitle(request: SdkElicitationRequest, field: ParsedTextInputField): string {
|
|
432
|
+
const parts = [
|
|
433
|
+
request.serverName ? `[${request.serverName}]` : "",
|
|
434
|
+
field.title,
|
|
435
|
+
field.description,
|
|
436
|
+
].filter((part) => typeof part === "string" && part.trim().length > 0);
|
|
437
|
+
return parts.join("\n\n");
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function buildTextInputPlaceholder(field: ParsedTextInputField): string | undefined {
|
|
441
|
+
const desc = field.description.trim();
|
|
442
|
+
if (!desc) return field.required ? "Required" : "Leave empty to skip";
|
|
443
|
+
|
|
444
|
+
const formatLine = desc
|
|
445
|
+
.split(/\r?\n/)
|
|
446
|
+
.map((line) => line.trim())
|
|
447
|
+
.find((line) => /^format:/i.test(line));
|
|
448
|
+
|
|
449
|
+
if (!formatLine) return field.required ? "Required" : "Leave empty to skip";
|
|
450
|
+
const hint = formatLine.replace(/^format:\s*/i, "").trim();
|
|
451
|
+
return hint.length > 0 ? hint : field.required ? "Required" : "Leave empty to skip";
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async function promptTextInputElicitation(
|
|
455
|
+
request: SdkElicitationRequest,
|
|
456
|
+
fields: ParsedTextInputField[],
|
|
457
|
+
ui: ExtensionUIContext,
|
|
458
|
+
signal: AbortSignal,
|
|
459
|
+
): Promise<SdkElicitationResult> {
|
|
460
|
+
const content: Record<string, string | string[]> = {};
|
|
461
|
+
|
|
462
|
+
for (const field of fields) {
|
|
463
|
+
const value = await ui.input(
|
|
464
|
+
buildTextInputPromptTitle(request, field),
|
|
465
|
+
buildTextInputPlaceholder(field),
|
|
466
|
+
{ signal, ...(field.secure ? { secure: true } : {}) },
|
|
467
|
+
);
|
|
468
|
+
if (value === undefined) {
|
|
469
|
+
return { action: "cancel" };
|
|
470
|
+
}
|
|
471
|
+
content[field.id] = value;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return { action: "accept", content };
|
|
475
|
+
}
|
|
476
|
+
|
|
358
477
|
export function createClaudeCodeElicitationHandler(
|
|
359
478
|
ui: ExtensionUIContext | undefined,
|
|
360
479
|
): ((request: SdkElicitationRequest, options: { signal: AbortSignal }) => Promise<SdkElicitationResult>) | undefined {
|
|
@@ -366,19 +485,24 @@ export function createClaudeCodeElicitationHandler(
|
|
|
366
485
|
}
|
|
367
486
|
|
|
368
487
|
const questions = parseAskUserQuestionsElicitation(request);
|
|
369
|
-
if (
|
|
370
|
-
|
|
488
|
+
if (questions) {
|
|
489
|
+
const interviewResult = await showInterviewRound(questions, { signal }, { ui } as any).catch(() => undefined);
|
|
490
|
+
if (interviewResult && Object.keys(interviewResult.answers).length > 0) {
|
|
491
|
+
return {
|
|
492
|
+
action: "accept",
|
|
493
|
+
content: roundResultToElicitationContent(questions, interviewResult),
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return promptElicitationWithDialogs(request, questions, ui, signal);
|
|
371
498
|
}
|
|
372
499
|
|
|
373
|
-
const
|
|
374
|
-
if (
|
|
375
|
-
return
|
|
376
|
-
action: "accept",
|
|
377
|
-
content: roundResultToElicitationContent(questions, interviewResult),
|
|
378
|
-
};
|
|
500
|
+
const textFields = parseTextInputElicitation(request);
|
|
501
|
+
if (textFields) {
|
|
502
|
+
return promptTextInputElicitation(request, textFields, ui, signal);
|
|
379
503
|
}
|
|
380
504
|
|
|
381
|
-
return
|
|
505
|
+
return { action: "decline" };
|
|
382
506
|
};
|
|
383
507
|
}
|
|
384
508
|
|
|
@@ -508,15 +632,15 @@ export function extractToolResultsFromSdkUserMessage(message: SDKUserMessage): A
|
|
|
508
632
|
return extracted;
|
|
509
633
|
}
|
|
510
634
|
|
|
511
|
-
function
|
|
512
|
-
|
|
635
|
+
function attachExternalResultsToToolBlocks(
|
|
636
|
+
toolBlocks: AssistantMessage["content"],
|
|
513
637
|
toolResultsById: ReadonlyMap<string, ExternalToolResultPayload>,
|
|
514
638
|
): void {
|
|
515
|
-
for (const block of
|
|
516
|
-
if (block.type !== "toolCall") continue;
|
|
639
|
+
for (const block of toolBlocks) {
|
|
640
|
+
if (block.type !== "toolCall" && block.type !== "serverToolUse") continue;
|
|
517
641
|
const externalResult = toolResultsById.get(block.id);
|
|
518
642
|
if (!externalResult) continue;
|
|
519
|
-
(block as ToolCallWithExternalResult).externalResult = externalResult;
|
|
643
|
+
(block as ToolCallWithExternalResult & { id: string }).externalResult = externalResult;
|
|
520
644
|
}
|
|
521
645
|
}
|
|
522
646
|
|
|
@@ -554,8 +678,8 @@ async function pumpSdkMessages(
|
|
|
554
678
|
/** Track the last text content seen across all assistant turns for the final message. */
|
|
555
679
|
let lastTextContent = "";
|
|
556
680
|
let lastThinkingContent = "";
|
|
557
|
-
/** Collect tool
|
|
558
|
-
const
|
|
681
|
+
/** Collect tool blocks from intermediate SDK turns for tool execution rendering. */
|
|
682
|
+
const intermediateToolBlocks: AssistantMessage["content"] = [];
|
|
559
683
|
/** Preserve real external tool results from Claude Code's synthetic user messages. */
|
|
560
684
|
const toolResultsById = new Map<string, ExternalToolResultPayload>();
|
|
561
685
|
|
|
@@ -666,9 +790,9 @@ async function pumpSdkMessages(
|
|
|
666
790
|
lastTextContent = block.text;
|
|
667
791
|
} else if (block.type === "thinking" && block.thinking) {
|
|
668
792
|
lastThinkingContent = block.thinking;
|
|
669
|
-
} else if (block.type === "toolCall") {
|
|
670
|
-
// Collect tool
|
|
671
|
-
|
|
793
|
+
} else if (block.type === "toolCall" || block.type === "serverToolUse") {
|
|
794
|
+
// Collect tool blocks for externalToolExecution rendering
|
|
795
|
+
intermediateToolBlocks.push(block);
|
|
672
796
|
}
|
|
673
797
|
}
|
|
674
798
|
}
|
|
@@ -678,24 +802,33 @@ async function pumpSdkMessages(
|
|
|
678
802
|
for (const { toolUseId, result } of extractToolResultsFromSdkUserMessage(msg as SDKUserMessage)) {
|
|
679
803
|
toolResultsById.set(toolUseId, result);
|
|
680
804
|
}
|
|
681
|
-
|
|
805
|
+
attachExternalResultsToToolBlocks(intermediateToolBlocks, toolResultsById);
|
|
682
806
|
|
|
683
807
|
// Push a synthetic toolcall_end for each tool call from this turn
|
|
684
808
|
// so the TUI can render tool results in real-time during the SDK
|
|
685
809
|
// session instead of waiting until the entire session completes.
|
|
686
810
|
if (builder) {
|
|
687
811
|
for (const block of builder.message.content) {
|
|
688
|
-
if (block.type !== "toolCall") continue;
|
|
689
812
|
const extResult = (block as ToolCallWithExternalResult).externalResult;
|
|
690
813
|
if (!extResult) continue;
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
814
|
+
const contentIndex = builder.message.content.indexOf(block);
|
|
815
|
+
if (contentIndex < 0) continue;
|
|
816
|
+
// Push synthetic completion events with result attached so the
|
|
817
|
+
// chat-controller can update pending ToolExecutionComponents.
|
|
818
|
+
if (block.type === "toolCall") {
|
|
819
|
+
stream.push({
|
|
820
|
+
type: "toolcall_end",
|
|
821
|
+
contentIndex,
|
|
822
|
+
toolCall: block,
|
|
823
|
+
partial: builder.message,
|
|
824
|
+
});
|
|
825
|
+
} else if (block.type === "serverToolUse") {
|
|
826
|
+
stream.push({
|
|
827
|
+
type: "server_tool_use",
|
|
828
|
+
contentIndex,
|
|
829
|
+
partial: builder.message,
|
|
830
|
+
});
|
|
831
|
+
}
|
|
699
832
|
}
|
|
700
833
|
}
|
|
701
834
|
|
|
@@ -713,8 +846,8 @@ async function pumpSdkMessages(
|
|
|
713
846
|
const finalContent: AssistantMessage["content"] = [];
|
|
714
847
|
|
|
715
848
|
// Add tool calls from intermediate turns first (renders above text)
|
|
716
|
-
|
|
717
|
-
finalContent.push(...
|
|
849
|
+
attachExternalResultsToToolBlocks(intermediateToolBlocks, toolResultsById);
|
|
850
|
+
finalContent.push(...intermediateToolBlocks);
|
|
718
851
|
|
|
719
852
|
// Add text/thinking from the last turn
|
|
720
853
|
if (builder && builder.message.content.length > 0) {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
extractToolResultsFromSdkUserMessage,
|
|
12
12
|
getClaudeLookupCommand,
|
|
13
13
|
parseAskUserQuestionsElicitation,
|
|
14
|
+
parseTextInputElicitation,
|
|
14
15
|
parseClaudeLookupOutput,
|
|
15
16
|
roundResultToElicitationContent,
|
|
16
17
|
} from "../stream-adapter.ts";
|
|
@@ -514,6 +515,117 @@ describe("stream-adapter — MCP elicitation bridge", () => {
|
|
|
514
515
|
},
|
|
515
516
|
});
|
|
516
517
|
});
|
|
518
|
+
|
|
519
|
+
test("parseTextInputElicitation recognizes secure free-text MCP forms", () => {
|
|
520
|
+
const request = {
|
|
521
|
+
serverName: "gsd-workflow",
|
|
522
|
+
message: "Enter values for environment variables.",
|
|
523
|
+
mode: "form" as const,
|
|
524
|
+
requestedSchema: {
|
|
525
|
+
type: "object" as const,
|
|
526
|
+
properties: {
|
|
527
|
+
TEST_PASSWORD: {
|
|
528
|
+
type: "string",
|
|
529
|
+
title: "TEST_PASSWORD",
|
|
530
|
+
description: "Format: min 8 characters\nLeave empty to skip.",
|
|
531
|
+
},
|
|
532
|
+
PROJECT_NAME: {
|
|
533
|
+
type: "string",
|
|
534
|
+
title: "PROJECT_NAME",
|
|
535
|
+
description: "Human-readable project name.",
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
const parsed = parseTextInputElicitation(request as any);
|
|
542
|
+
assert.deepEqual(parsed, [
|
|
543
|
+
{
|
|
544
|
+
id: "TEST_PASSWORD",
|
|
545
|
+
title: "TEST_PASSWORD",
|
|
546
|
+
description: "Format: min 8 characters\nLeave empty to skip.",
|
|
547
|
+
required: false,
|
|
548
|
+
secure: true,
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
id: "PROJECT_NAME",
|
|
552
|
+
title: "PROJECT_NAME",
|
|
553
|
+
description: "Human-readable project name.",
|
|
554
|
+
required: false,
|
|
555
|
+
secure: false,
|
|
556
|
+
},
|
|
557
|
+
]);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
test("parseTextInputElicitation accepts legacy keys schema and skips unsupported fields", () => {
|
|
561
|
+
const request = {
|
|
562
|
+
serverName: "gsd-workflow",
|
|
563
|
+
message: "Enter secure values",
|
|
564
|
+
mode: "form" as const,
|
|
565
|
+
requestedSchema: {
|
|
566
|
+
type: "object" as const,
|
|
567
|
+
keys: {
|
|
568
|
+
API_TOKEN: {
|
|
569
|
+
type: "string",
|
|
570
|
+
title: "API_TOKEN",
|
|
571
|
+
description: "Leave empty to skip.",
|
|
572
|
+
},
|
|
573
|
+
META: {
|
|
574
|
+
type: "object",
|
|
575
|
+
title: "metadata",
|
|
576
|
+
},
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
const parsed = parseTextInputElicitation(request as any);
|
|
582
|
+
assert.deepEqual(parsed, [
|
|
583
|
+
{
|
|
584
|
+
id: "API_TOKEN",
|
|
585
|
+
title: "API_TOKEN",
|
|
586
|
+
description: "Leave empty to skip.",
|
|
587
|
+
required: false,
|
|
588
|
+
secure: true,
|
|
589
|
+
},
|
|
590
|
+
]);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
test("createClaudeCodeElicitationHandler collects secure_env_collect fields through input dialogs", async () => {
|
|
594
|
+
const secureRequest = {
|
|
595
|
+
serverName: "gsd-workflow",
|
|
596
|
+
message: "Enter values for environment variables.",
|
|
597
|
+
mode: "form" as const,
|
|
598
|
+
requestedSchema: {
|
|
599
|
+
type: "object" as const,
|
|
600
|
+
properties: {
|
|
601
|
+
TEST_PASSWORD: {
|
|
602
|
+
type: "string",
|
|
603
|
+
title: "TEST_PASSWORD",
|
|
604
|
+
description: "Format: Your secure testing password\nLeave empty to skip.",
|
|
605
|
+
},
|
|
606
|
+
},
|
|
607
|
+
},
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
const inputCalls: Array<{ opts?: { secure?: boolean } }> = [];
|
|
611
|
+
const handler = createClaudeCodeElicitationHandler({
|
|
612
|
+
input: async (_title: string, _placeholder?: string, opts?: { secure?: boolean }) => {
|
|
613
|
+
inputCalls.push({ opts });
|
|
614
|
+
return "super-secret";
|
|
615
|
+
},
|
|
616
|
+
} as any);
|
|
617
|
+
assert.ok(handler);
|
|
618
|
+
|
|
619
|
+
const result = await handler!(secureRequest as any, { signal: new AbortController().signal });
|
|
620
|
+
assert.deepEqual(result, {
|
|
621
|
+
action: "accept",
|
|
622
|
+
content: {
|
|
623
|
+
TEST_PASSWORD: "super-secret",
|
|
624
|
+
},
|
|
625
|
+
});
|
|
626
|
+
assert.equal(inputCalls.length, 1);
|
|
627
|
+
assert.equal(inputCalls[0]?.opts?.secure, true, "secure_env_collect fields should request secure input");
|
|
628
|
+
});
|
|
517
629
|
});
|
|
518
630
|
|
|
519
631
|
describe("stream-adapter — Windows Claude path lookup (#3770)", () => {
|
|
@@ -126,7 +126,7 @@ async function collectOneSecret(
|
|
|
126
126
|
): Promise<string | null> {
|
|
127
127
|
if (!ctx.hasUI) return null;
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
const customResult = await ctx.ui.custom((tui: any, theme: any, _kb: any, done: (r: string | null) => void) => {
|
|
130
130
|
let value = "";
|
|
131
131
|
let cachedLines: string[] | undefined;
|
|
132
132
|
|
|
@@ -223,6 +223,29 @@ async function collectOneSecret(
|
|
|
223
223
|
handleInput,
|
|
224
224
|
};
|
|
225
225
|
});
|
|
226
|
+
|
|
227
|
+
// RPC/web surfaces may not implement ctx.ui.custom(). Fall back to a
|
|
228
|
+
// standard input prompt so users can still provide the secret.
|
|
229
|
+
if (customResult !== undefined) {
|
|
230
|
+
return customResult;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (typeof ctx.ui?.input !== "function") {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const inputTitle = `Secure value for ${keyName} (${pageIndex + 1}/${totalPages})`;
|
|
238
|
+
const inputPlaceholder = hint || "Enter secret value";
|
|
239
|
+
const inputResult = await ctx.ui.input(
|
|
240
|
+
inputTitle,
|
|
241
|
+
inputPlaceholder,
|
|
242
|
+
{ secure: true },
|
|
243
|
+
);
|
|
244
|
+
if (typeof inputResult !== "string") {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
const trimmed = inputResult.trim();
|
|
248
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
226
249
|
}
|
|
227
250
|
|
|
228
251
|
/**
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
import { injectContext } from "./context-injector.js";
|
|
35
35
|
import type { WorkflowDefinition, StepDefinition } from "./definition-loader.js";
|
|
36
36
|
import { parseUnitId } from "./unit-id.js";
|
|
37
|
+
import { withFileLock } from "./file-lock.js";
|
|
37
38
|
|
|
38
39
|
/** Read and parse the frozen DEFINITION.yaml from a run directory. */
|
|
39
40
|
export function readFrozenDefinition(runDir: string): WorkflowDefinition {
|
|
@@ -179,24 +180,28 @@ export class CustomWorkflowEngine implements WorkflowEngine {
|
|
|
179
180
|
state: EngineState,
|
|
180
181
|
completedStep: CompletedStep,
|
|
181
182
|
): Promise<ReconcileResult> {
|
|
182
|
-
|
|
183
|
-
// workflow edits with a stale in-memory snapshot from deriveState().
|
|
184
|
-
const graph = readGraph(this.runDir);
|
|
183
|
+
const graphPath = join(this.runDir, "GRAPH.yaml");
|
|
185
184
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
185
|
+
return await withFileLock(graphPath, () => {
|
|
186
|
+
// Re-read the graph from disk so we do not overwrite concurrent
|
|
187
|
+
// workflow edits with a stale in-memory snapshot from deriveState().
|
|
188
|
+
const graph = readGraph(this.runDir);
|
|
189
189
|
|
|
190
|
-
|
|
191
|
-
|
|
190
|
+
// Extract stepId from "<workflowName>/<stepId>"
|
|
191
|
+
const { milestone, slice, task } = parseUnitId(completedStep.unitId);
|
|
192
|
+
const stepId = task ?? slice ?? milestone;
|
|
192
193
|
|
|
193
|
-
|
|
194
|
-
(
|
|
195
|
-
);
|
|
194
|
+
const updatedGraph = markStepComplete(graph, stepId);
|
|
195
|
+
writeGraph(this.runDir, updatedGraph);
|
|
196
196
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
197
|
+
const allDone = updatedGraph.steps.every(
|
|
198
|
+
(s) => s.status === "complete" || s.status === "expanded",
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
outcome: allDone ? "milestone-complete" : "continue",
|
|
203
|
+
};
|
|
204
|
+
});
|
|
200
205
|
}
|
|
201
206
|
|
|
202
207
|
/**
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
function _require(name: string) {
|
|
4
|
+
try {
|
|
5
|
+
return require(name);
|
|
6
|
+
} catch {
|
|
7
|
+
try {
|
|
8
|
+
const gsdPiRequire = require("module").createRequire(
|
|
9
|
+
require("path").join(process.cwd(), "node_modules", "gsd-pi", "index.js")
|
|
10
|
+
);
|
|
11
|
+
return gsdPiRequire(name);
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function withFileLockSync<T>(filePath: string, fn: () => T): T {
|
|
19
|
+
const lockfile = _require("proper-lockfile");
|
|
20
|
+
if (!lockfile) return fn();
|
|
21
|
+
|
|
22
|
+
if (!existsSync(filePath)) return fn();
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const release = lockfile.lockSync(filePath, { retries: 5, stale: 10000 });
|
|
26
|
+
try {
|
|
27
|
+
return fn();
|
|
28
|
+
} finally {
|
|
29
|
+
release();
|
|
30
|
+
}
|
|
31
|
+
} catch (err: any) {
|
|
32
|
+
if (err.code === "ELOCKED") {
|
|
33
|
+
// Could not get lock after retries, let's fallback to un-locked instead of crashing the whole state machine
|
|
34
|
+
return fn();
|
|
35
|
+
}
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function withFileLock<T>(filePath: string, fn: () => Promise<T> | T): Promise<T> {
|
|
41
|
+
const lockfile = _require("proper-lockfile");
|
|
42
|
+
if (!lockfile) return await fn();
|
|
43
|
+
|
|
44
|
+
if (!existsSync(filePath)) return await fn();
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const release = await lockfile.lock(filePath, { retries: 5, stale: 10000 });
|
|
48
|
+
try {
|
|
49
|
+
return await fn();
|
|
50
|
+
} finally {
|
|
51
|
+
await release();
|
|
52
|
+
}
|
|
53
|
+
} catch (err: any) {
|
|
54
|
+
if (err.code === "ELOCKED") {
|
|
55
|
+
return await fn();
|
|
56
|
+
}
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
}
|