@hadooppei/hwcode 1.0.7 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/.pi/extensions/command-filter.ts +2 -3
  2. package/.pi/extensions/cwd.ts +1 -4
  3. package/.pi/extensions/knowledge.ts +224 -0
  4. package/.pi/extensions/workflows/cloud/activation.ts +57 -43
  5. package/.pi/extensions/workflows/cloud/commands.ts +1 -86
  6. package/.pi/extensions/workflows/cloud/events.ts +16 -19
  7. package/.pi/extensions/workflows/cloud/interactions.ts +102 -0
  8. package/.pi/extensions/workflows/cloud/provider-tools.ts +15 -9
  9. package/.pi/extensions/workflows/cloud/runner-tools.ts +21 -20
  10. package/.pi/extensions/workflows/cloud/runtime.ts +41 -4
  11. package/.pi/extensions/workflows/cloud/terraform-tools.ts +43 -30
  12. package/.pi/extensions/workflows/sdd.ts +2 -1
  13. package/.pi/extensions/workflows/vibe.ts +2 -1
  14. package/.pi/extensions/workflows/workspace-guard.ts +1 -5
  15. package/.pi/lib/extension-ui.ts +52 -0
  16. package/.pi/lib/knowledge/extractor.ts +35 -0
  17. package/.pi/lib/knowledge/matcher.ts +122 -0
  18. package/.pi/lib/knowledge/review-worker.ts +260 -0
  19. package/.pi/lib/knowledge/sanitize.ts +26 -0
  20. package/.pi/lib/knowledge/session-scanner.ts +155 -0
  21. package/.pi/lib/knowledge/store.ts +365 -0
  22. package/.pi/lib/knowledge/types.ts +91 -0
  23. package/.pi/lib/knowledge/worker-protocol.ts +20 -0
  24. package/.pi/lib/runtime/defaults.ts +31 -0
  25. package/.pi/lib/runtime/paths.ts +33 -16
  26. package/.pi/lib/tool-result.ts +7 -0
  27. package/.pi/lib/workflows/cloud/bundles.ts +73 -40
  28. package/.pi/lib/workflows/cloud/workspace.ts +6 -0
  29. package/.pi/lib/workflows/state.ts +6 -26
  30. package/.pi/skills/hwcode-cloud/SKILL.md +2 -2
  31. package/README.md +36 -31
  32. package/bin/hwcode.js +2 -3
  33. package/package.json +1 -1
  34. package/.pi/extensions/workflows/cloud/shared.ts +0 -230
  35. package/.pi/lib/workflows/cloud/template-save.ts +0 -108
  36. package/.pi/lib/workflows/cloud/templates.ts +0 -314
@@ -1,230 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import { isAbsolute, resolve } from "node:path";
3
-
4
- import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
5
- import { CURSOR_MARKER, Input, type Component, type TUI } from "@earendil-works/pi-tui";
6
-
7
- import { isCloudRunWorkspace } from "../../../lib/workflows/cloud/workspace.ts";
8
- import { type RemoteTrustInteraction } from "../../../lib/workflows/cloud/remote/connect.ts";
9
- import { getCloudProvider, CLOUD_PROVIDERS, redactCredentialValues, type CloudCredentials, type CloudOperation, type CloudVendorId } from "../../../lib/workflows/cloud/providers.ts";
10
- import {
11
- cloudVaultExists, createEmptyVault, defaultCloudVaultPath, readCloudVault,
12
- } from "../../../lib/workflows/cloud/vault.ts";
13
- import { WORKFLOW_STATE_TYPE, activeWorkflow, type CloudWorkflowDetails, type WorkflowState } from "../../../lib/workflows/state.ts";
14
-
15
- export const CLOUD_AUDIT_TYPE = "hwcode-cloud-audit";
16
- export const ORCHESTRATION_COMMANDS = new Set(["terraform", "tofu", "pulumi", "kubectl", "helm"]);
17
- export const SENSITIVE_ARGUMENT = /^--?(?:access[-_]?key|secret(?:[-_]?(?:access|key))?|client[-_]?secret|password|credential|token)(?:=|$)/iu;
18
- const LEGACY_CLOUD_STATE_TYPE = "hwcode-cloud-state";
19
-
20
- interface LegacyCloudWorkflowState {
21
- version: 1;
22
- active: true;
23
- root: string;
24
- vendor: CloudVendorId;
25
- deployCurrentProject: boolean;
26
- request: string;
27
- allowNonDeleteChanges: boolean;
28
- failedApproaches: CloudWorkflowDetails["failedApproaches"];
29
- terminalFailure: boolean;
30
- activatedAt: string;
31
- }
32
-
33
- class MaskedInput implements Component {
34
- private readonly input = new Input();
35
- private _focused = true;
36
- private readonly tui: TUI;
37
- private readonly theme: Theme;
38
- private readonly title: string;
39
-
40
- constructor(
41
- tui: TUI,
42
- theme: Theme,
43
- title: string,
44
- done: (value: string | undefined) => void,
45
- ) {
46
- this.tui = tui;
47
- this.theme = theme;
48
- this.title = title;
49
- this.input.onSubmit = (value) => done(value);
50
- this.input.onEscape = () => done(undefined);
51
- this.input.focused = true;
52
- }
53
-
54
- get focused(): boolean { return this._focused; }
55
- set focused(value: boolean) { this._focused = value; this.input.focused = value; }
56
- handleInput(data: string): void { this.input.handleInput(data); this.tui.requestRender(); }
57
- invalidate(): void { this.input.invalidate(); }
58
- render(width: number): string[] {
59
- const masked = "•".repeat(Math.min(Array.from(this.input.getValue()).length, Math.max(1, width - 1)));
60
- return [this.theme.fg("accent", this.title), `${masked}${CURSOR_MARKER}`, this.theme.fg("dim", "Enter 确认 · Esc 取消 · 输入内容不会显示")];
61
- }
62
- }
63
-
64
- export function notify(ctx: ExtensionContext, message: string, level: "info" | "warning" | "error" = "info"): void {
65
- if (ctx.hasUI) ctx.ui.notify(message, level);
66
- }
67
-
68
- export function cloudDetails(state: WorkflowState): CloudWorkflowDetails | undefined {
69
- return state.mode === "cloud" ? state.details : undefined;
70
- }
71
-
72
- export function cloudArtifactDirectory(state: WorkflowState): string {
73
- const path = cloudDetails(state)?.artifactDirectory;
74
- return path && isCloudRunWorkspace(state.root, path) ? path : state.root;
75
- }
76
-
77
- export function formatTemplateTime(value: string): string {
78
- const date = new Date(value);
79
- if (Number.isNaN(date.valueOf())) return value;
80
- const pad = (part: number) => String(part).padStart(2, "0");
81
- return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
82
- }
83
-
84
- export function restoreCloudWorkflow(ctx: ExtensionContext): WorkflowState | undefined {
85
- const state = activeWorkflow(ctx.sessionManager.getEntries());
86
- return state?.mode === "cloud" && state.details ? state : undefined;
87
- }
88
-
89
- function decodeLegacyCloudState(value: unknown): LegacyCloudWorkflowState | undefined {
90
- if (!value || typeof value !== "object") return undefined;
91
- const data = value as Record<string, unknown>;
92
- if (data.version !== 1 || data.active !== true || typeof data.root !== "string"
93
- || typeof data.vendor !== "string" || !CLOUD_PROVIDERS.some((provider) => provider.id === data.vendor)
94
- || typeof data.deployCurrentProject !== "boolean" || typeof data.request !== "string"
95
- || typeof data.allowNonDeleteChanges !== "boolean" || !Array.isArray(data.failedApproaches)
96
- || typeof data.terminalFailure !== "boolean" || typeof data.activatedAt !== "string") return undefined;
97
- return data as unknown as LegacyCloudWorkflowState;
98
- }
99
-
100
- export function restoreLegacyCloudWorkflow(ctx: ExtensionContext): WorkflowState | undefined {
101
- for (const entry of [...ctx.sessionManager.getEntries()].reverse()) {
102
- if (entry.type !== "custom" || entry.customType !== LEGACY_CLOUD_STATE_TYPE) continue;
103
- const legacy = decodeLegacyCloudState(entry.data);
104
- if (!legacy) return undefined;
105
- return {
106
- version: 2, status: "active", mode: "cloud", root: legacy.root,
107
- phase: legacy.terminalFailure ? "failed" : "connected",
108
- activatedAt: legacy.activatedAt, updatedAt: legacy.activatedAt,
109
- details: {
110
- vendor: legacy.vendor, deployCurrentProject: legacy.deployCurrentProject,
111
- request: legacy.request, allowNonDeleteChanges: legacy.allowNonDeleteChanges,
112
- failedApproaches: legacy.failedApproaches, successfulSteps: [], terminalFailure: legacy.terminalFailure,
113
- },
114
- };
115
- }
116
- return undefined;
117
- }
118
-
119
- export function appendWorkflowState(pi: ExtensionAPI, state: WorkflowState): void {
120
- pi.appendEntry<WorkflowState>(WORKFLOW_STATE_TYPE, state);
121
- }
122
-
123
- async function secretInput(ctx: ExtensionContext, title: string): Promise<string | undefined> {
124
- if (ctx.mode !== "tui") return undefined;
125
- return ctx.ui.custom<string | undefined>((tui, theme, _keybindings, done) => new MaskedInput(tui, theme, title, done));
126
- }
127
-
128
- export async function unlockVault(ctx: ExtensionCommandContext): Promise<{ password: string; payload: ReturnType<typeof createEmptyVault> } | undefined> {
129
- const vaultPath = defaultCloudVaultPath();
130
- if (!cloudVaultExists(vaultPath)) {
131
- while (true) {
132
- const password = await secretInput(ctx, "创建云凭据主密码(至少 8 个字符)");
133
- if (password === undefined) return undefined;
134
- if (password.length < 8) { notify(ctx, "主密码至少需要 8 个字符。", "warning"); continue; }
135
- const confirmation = await secretInput(ctx, "再次输入主密码");
136
- if (confirmation === undefined) return undefined;
137
- if (confirmation !== password) { notify(ctx, "两次输入的主密码不一致。", "warning"); continue; }
138
- return { password, payload: createEmptyVault() };
139
- }
140
- }
141
- while (true) {
142
- const password = await secretInput(ctx, "输入云凭据主密码以解锁");
143
- if (password === undefined) return undefined;
144
- try { return { password, payload: readCloudVault(password, vaultPath) }; }
145
- catch (error) {
146
- notify(ctx, error instanceof Error ? error.message : String(error), "error");
147
- if (!(await ctx.ui.confirm("重新输入主密码?", "凭据未解锁,HWCode Cloud 尚未启动。"))) return undefined;
148
- }
149
- }
150
- }
151
-
152
- export async function collectCredentials(vendor: CloudVendorId, root: string, ctx: ExtensionCommandContext): Promise<CloudCredentials | undefined> {
153
- const credentials: CloudCredentials = {};
154
- for (const field of getCloudProvider(vendor).credentialFields) {
155
- let value = field.secret ? await secretInput(ctx, field.label) : await ctx.ui.input(field.label, field.placeholder);
156
- if (value === undefined) return undefined;
157
- value = value.trim();
158
- if (!value && !field.optional) { notify(ctx, `${field.label} 不能为空。`, "warning"); return collectCredentials(vendor, root, ctx); }
159
- if (!value) continue;
160
- if (field.fileContents) {
161
- const path = isAbsolute(value) ? value : resolve(root, value);
162
- try { const contents = readFileSync(path, "utf8"); JSON.parse(contents); credentials[field.key] = contents; }
163
- catch (error) {
164
- notify(ctx, `无法读取有效的 JSON 凭据文件 ${path}: ${error instanceof Error ? error.message : String(error)}`, "error");
165
- return collectCredentials(vendor, root, ctx);
166
- }
167
- } else credentials[field.key] = value;
168
- }
169
- if (vendor === "gcp") {
170
- const account = JSON.parse(credentials.serviceAccountJson) as Record<string, unknown>;
171
- credentials.projectId ||= typeof account.project_id === "string" ? account.project_id : "";
172
- if (!credentials.projectId) { notify(ctx, "Service Account JSON 中没有 project_id,请重新输入。", "error"); return collectCredentials(vendor, root, ctx); }
173
- }
174
- return credentials;
175
- }
176
-
177
- export function activationPrompt(state: WorkflowState): string {
178
- const details = cloudDetails(state)!;
179
- const provider = getCloudProvider(details.vendor);
180
- const guidance = details.templateGuidance
181
- ? `\n\nReusable template guidance (${details.sourceTemplate?.name ?? "saved template"}):\n${details.templateGuidance}`
182
- : "";
183
- const runner = details.runner?.name ?? (details.runnerPreference === "automatic" ? "automatic temporary Runner requested" : "not selected");
184
- return `/skill:hwcode-cloud HWCode Cloud workflow activated.
185
-
186
- Locked project root: ${state.root}
187
- Task artifact workspace: ${cloudArtifactDirectory(state)}
188
- Cloud provider: ${getCloudProvider(details.vendor).label}
189
- Terraform Runner: ${runner}(Note: Runner is only for Terraform; regular discovery and CLI queries run locally via ${provider.cli})
190
- Deploy current project: ${details.deployCurrentProject ? "yes" : "no"}
191
- User objective:\n${details.request}${guidance}
192
-
193
- Note: If the cloud CLI is unavailable on this machine, you may use Python scripts, Node.js SDKs, or curl to invoke the cloud provider's REST APIs directly. Otherwise, prefer hwcode_cloud_exec.
194
- `;
195
- }
196
-
197
- let approvalQueue = Promise.resolve();
198
- export async function approveOperation(operation: CloudOperation, params: { command: string; args: string[]; intent: string }, state: WorkflowState, ctx: ExtensionContext): Promise<"allow" | "allow-session" | "deny"> {
199
- if (operation === "read") return "allow";
200
- if (!ctx.hasUI) return "deny";
201
- const next = approvalQueue.then(async () => {
202
- const details = cloudDetails(state)!;
203
- const command = `${params.command} ${params.args.join(" ")}`;
204
- if (operation === "delete") return await ctx.ui.confirm("确认删除云资源?", `目标:${params.intent}\n命令:${command}\n\n删除操作始终逐次确认。`) ? "allow" : "deny";
205
- if (details.allowNonDeleteChanges) return "allow";
206
- const choice = await ctx.ui.select("允许修改云账号内资源?", ["仅允许本次", "允许本会话后续非删除变更,不再询问", "拒绝"]);
207
- if (choice === "仅允许本次") return "allow";
208
- if (choice === "允许本会话后续非删除变更,不再询问") return "allow-session";
209
- return "deny";
210
- });
211
-
212
- approvalQueue = next.then(() => {}, () => {});
213
- return await next;
214
- }
215
-
216
- export function remoteTrustInteraction(ctx: ExtensionContext): RemoteTrustInteraction {
217
- return {
218
- confirm: async (host, port, keys, role) => ctx.hasUI && ctx.ui.confirm(
219
- role === "jump" ? "信任 SSH 跳板机主机密钥?" : "信任 Terraform Runner 主机密钥?",
220
- `主机:${host}:${port}\n${keys.map((key) => `${key.algorithm} ${key.fingerprint}`).join("\n")}\n\n请仅在指纹与云控制台或可信运维记录一致时确认。`,
221
- ),
222
- };
223
- }
224
-
225
- export function safeStepText(value: string, credentials: CloudCredentials): string {
226
- return redactCredentialValues(value.replace(/[\r\n]+/gu, " ").slice(0, 2_000), credentials);
227
- }
228
-
229
- export const terraformError = (message: string) => ({ content: [{ type: "text" as const, text: message }], isError: true, details: {} });
230
- export const terraformOk = (message: string, details: Record<string, unknown> = {}) => ({ content: [{ type: "text" as const, text: message }], details });
@@ -1,108 +0,0 @@
1
- import type { CloudPromptTemplate } from "./templates.ts";
2
- import { cloudPromptTemplateSource } from "./templates.ts";
3
- import type { CloudWorkflowDetails, WorkflowState } from "../state.ts";
4
- import { CLOUD_RUNTIME_DEFAULTS } from "../../runtime/defaults.ts";
5
-
6
- export type CloudTemplateSourceAction = "update" | "create" | "cancel";
7
- export type CloudTemplateSaveStage = "source-action" | "name" | "notes";
8
-
9
- export interface CloudTemplateSaveInteraction {
10
- chooseSourceAction(source: CloudPromptTemplate): Promise<CloudTemplateSourceAction | undefined>;
11
- inputName(defaultName: string): Promise<string | undefined>;
12
- editNotes(initialNotes: string): Promise<string | undefined>;
13
- sourceMissing(sourceName: string): void | Promise<void>;
14
- }
15
-
16
- export interface CloudTemplateStore {
17
- list(): CloudPromptTemplate[];
18
- create(name: string, state: WorkflowState, notes: string): CloudPromptTemplate;
19
- update(template: CloudPromptTemplate, state: WorkflowState, notes: string): CloudPromptTemplate;
20
- }
21
-
22
- export type CloudTemplateSaveResult =
23
- | { status: "cancelled"; stage: CloudTemplateSaveStage }
24
- | {
25
- status: "saved";
26
- action: "created" | "updated";
27
- template: CloudPromptTemplate;
28
- details: CloudWorkflowDetails;
29
- };
30
-
31
- export interface SaveCloudWorkflowTemplateOptions {
32
- state: WorkflowState;
33
- requestedName?: string;
34
- interaction: CloudTemplateSaveInteraction;
35
- store: CloudTemplateStore;
36
- redact?: (value: string) => string;
37
- }
38
-
39
- /**
40
- * Coordinates the save/update decision without depending on Pi UI or filesystem APIs.
41
- * Storage is invoked only after every interactive step succeeds, so cancellation and
42
- * storage failures cannot partially change the workflow state.
43
- */
44
- export async function saveCloudWorkflowTemplate(
45
- options: SaveCloudWorkflowTemplateOptions,
46
- ): Promise<CloudTemplateSaveResult> {
47
- const { state, interaction, store } = options;
48
- if (state.mode !== "cloud" || !state.details) throw new Error("Cloud workflow state is required");
49
- if (state.details.successfulSteps.length === 0) {
50
- throw new Error("No successful Cloud execution steps are available to summarize");
51
- }
52
-
53
- const details = state.details;
54
- let target: CloudPromptTemplate | undefined;
55
- if (details.sourceTemplate) {
56
- target = store.list().find((template) => template.id === details.sourceTemplate?.id);
57
- if (target) {
58
- const sourceAction = await interaction.chooseSourceAction(target);
59
- if (!sourceAction || sourceAction === "cancel") {
60
- return { status: "cancelled", stage: "source-action" };
61
- }
62
- if (sourceAction === "create") target = undefined;
63
- } else {
64
- await interaction.sourceMissing(details.sourceTemplate.name);
65
- }
66
- }
67
-
68
- let name = target?.name;
69
- if (!name) {
70
- const suppliedName = options.requestedName?.trim();
71
- if (suppliedName) {
72
- name = suppliedName;
73
- } else {
74
- const defaultName = details.sourceTemplate
75
- ? `${details.sourceTemplate.name} 副本`
76
- : details.request.slice(0, CLOUD_RUNTIME_DEFAULTS.templates.slugMaxLength);
77
- name = (await interaction.inputName(defaultName))?.trim();
78
- }
79
- if (!name) return { status: "cancelled", stage: "name" };
80
- }
81
-
82
- const notes = await interaction.editNotes(target?.notes ?? "");
83
- if (notes === undefined) return { status: "cancelled", stage: "notes" };
84
-
85
- const redact = options.redact ?? ((value: string) => value);
86
- const safeName = redact(name);
87
- const safeNotes = redact(notes.trim());
88
- const safeState: WorkflowState = {
89
- ...state,
90
- details: {
91
- ...details,
92
- request: redact(details.request),
93
- templateGuidance: details.templateGuidance === undefined
94
- ? undefined
95
- : redact(details.templateGuidance),
96
- },
97
- };
98
- const template = target
99
- ? store.update(target, safeState, safeNotes)
100
- : store.create(safeName, safeState, safeNotes);
101
-
102
- return {
103
- status: "saved",
104
- action: target ? "updated" : "created",
105
- template,
106
- details: { ...details, sourceTemplate: cloudPromptTemplateSource(template) },
107
- };
108
- }
@@ -1,314 +0,0 @@
1
- import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { basename, dirname, join } from "node:path";
4
-
5
- import { getCloudProvider, isCloudVendorId, type CloudVendorId } from "./providers.ts";
6
- import type { CloudTemplateSource, WorkflowState } from "../state.ts";
7
- import { legacyUserCloudTemplatePaths, userRuntimePaths } from "../../runtime/paths.ts";
8
- import { CLOUD_RUNTIME_DEFAULTS } from "../../runtime/defaults.ts";
9
-
10
- export interface CloudPromptTemplate {
11
- id: string;
12
- name: string;
13
- description: string;
14
- createdAt: string;
15
- updatedAt: string;
16
- vendor: CloudVendorId;
17
- deployCurrentProject: boolean;
18
- objective: string;
19
- notes: string;
20
- body: string;
21
- path: string;
22
- }
23
-
24
- export function cloudPromptTemplateSource(template: CloudPromptTemplate): CloudTemplateSource {
25
- return {
26
- id: template.id,
27
- name: template.name,
28
- createdAt: template.createdAt,
29
- updatedAt: template.updatedAt,
30
- kind: "prompt",
31
- };
32
- }
33
-
34
- export function defaultCloudPromptDirectory(home = homedir()): string {
35
- return userRuntimePaths(home).cloudPromptTemplates;
36
- }
37
-
38
- function slugify(value: string): string {
39
- const slug = value
40
- .normalize("NFKD")
41
- .toLowerCase()
42
- .replace(/[^a-z0-9]+/gu, "-")
43
- .replace(/^-+|-+$/gu, "")
44
- .slice(0, CLOUD_RUNTIME_DEFAULTS.templates.slugMaxLength);
45
- return slug || `task-${new Date().toISOString().slice(0, 10)}`;
46
- }
47
-
48
- function frontmatterValue(text: string, key: string): string | undefined {
49
- const line = text.match(new RegExp(`^${key}:\\s*(.+)$`, "mu"))?.[1]?.trim();
50
- if (!line) return undefined;
51
- try {
52
- const value = JSON.parse(line) as unknown;
53
- return typeof value === "string" ? value : String(value);
54
- } catch {
55
- return line.replace(/^['"]|['"]$/gu, "");
56
- }
57
- }
58
-
59
- function normalizeTimestamp(value: string | undefined, fallback: string): string {
60
- if (!value) return fallback;
61
- const date = new Date(value);
62
- return Number.isNaN(date.valueOf()) ? fallback : date.toISOString();
63
- }
64
-
65
- function legacyTemplateName(id: string): string {
66
- const words = id.replace(/^hwcloud-/u, "").split("-").filter(Boolean);
67
- if (words.length === 0) return "Legacy Cloud Template";
68
- return words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
69
- }
70
-
71
- function cleanTemplateName(value: string): string {
72
- return value.replace(/[\r\n]+/gu, " ").replace(/\s+/gu, " ").trim().slice(0, CLOUD_RUNTIME_DEFAULTS.templates.nameMaxLength);
73
- }
74
-
75
- export function parseCloudPromptTemplate(path: string): CloudPromptTemplate | undefined {
76
- const text = readFileSync(path, "utf8");
77
- const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/u);
78
- if (!match) return undefined;
79
- const metadata = match[1];
80
- const vendorValue = frontmatterValue(metadata, "hwcode-cloud-vendor");
81
- if (!vendorValue || !isCloudVendorId(vendorValue)) return undefined;
82
- const id = basename(path, ".md");
83
- const stats = statSync(path);
84
- const modifiedAt = stats.mtime.toISOString();
85
- const bornAt = stats.birthtimeMs > 0 ? stats.birthtime.toISOString() : modifiedAt;
86
- const name = cleanTemplateName(frontmatterValue(metadata, "hwcode-cloud-name") ?? "") || legacyTemplateName(id);
87
- return {
88
- id,
89
- name,
90
- description: frontmatterValue(metadata, "description") ?? name,
91
- createdAt: normalizeTimestamp(frontmatterValue(metadata, "hwcode-cloud-created-at"), bornAt),
92
- updatedAt: normalizeTimestamp(frontmatterValue(metadata, "hwcode-cloud-updated-at"), modifiedAt),
93
- vendor: vendorValue,
94
- deployCurrentProject: frontmatterValue(metadata, "hwcode-cloud-deploy") === "true",
95
- objective: frontmatterValue(metadata, "hwcode-cloud-objective") ?? "",
96
- notes: frontmatterValue(metadata, "hwcode-cloud-notes") ?? "",
97
- body: match[2].trim(),
98
- path,
99
- };
100
- }
101
-
102
- export function listCloudPromptTemplates(
103
- directory?: string,
104
- home = homedir(),
105
- ): CloudPromptTemplate[] {
106
- const directories = directory
107
- ? [directory]
108
- : [...new Set([defaultCloudPromptDirectory(home), legacyUserCloudTemplatePaths(home).prompts])];
109
- return directories
110
- .filter(existsSync)
111
- .flatMap((path) => readdirSync(path, { withFileTypes: true })
112
- .filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
113
- .map((entry) => parseCloudPromptTemplate(join(path, entry.name))))
114
- .filter((template): template is CloudPromptTemplate => Boolean(template))
115
- .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt) || left.name.localeCompare(right.name));
116
- }
117
-
118
- export function expandCloudPromptTemplate(template: CloudPromptTemplate, argumentsText: string): string {
119
- const value = argumentsText.trim();
120
- return template.body.replace(/\$\{@:-([^}]*)\}/gu, (_match, fallback: string) => value || fallback);
121
- }
122
-
123
- function inlineCode(value: string): string {
124
- return value.replace(/[\r\n]+/gu, " ").replaceAll("`", "\\`");
125
- }
126
-
127
- function singleLine(value: string): string {
128
- return value.replace(/[\r\n]+/gu, " ").replace(/\s+/gu, " ").trim();
129
- }
130
-
131
- interface GeneralizedVar { placeholder: string; original: string; hint: string }
132
-
133
- const GENERALIZE_KEY_PATTERNS: Array<[RegExp, string, string]> = [
134
- [/(?:^|--)(?:cli-)?region$/iu, "region", "使用当前凭据 region"],
135
- [/vpc[-_]?id$/iu, "vpc-id", "ListVpcs 查询"],
136
- [/subnet[-_]?id$/iu, "subnet-id", "ListSubnets 查询"],
137
- [/image[-_]?id$/iu, "image-id", "ListImages 查询"],
138
- [/secur(?:ity)?[-_]?group[-_]?id$/iu, "sg-id", "ListSecurityGroups 查询"],
139
- [/key[-_]?(?:pair|name)$/iu, "key-name", "ListKeypairs 查询"],
140
- ];
141
-
142
- const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
143
- const HEX32_RE = /^[0-9a-f]{32}$/iu;
144
-
145
- function generalizeArgs(args: readonly string[]): { args: string[]; vars: GeneralizedVar[] } {
146
- const vars: GeneralizedVar[] = [];
147
- const seen = new Set<string>();
148
- const result = args.map((arg) => {
149
- const eq = arg.indexOf("=");
150
- if (eq > 0) {
151
- const key = arg.slice(0, eq);
152
- const val = arg.slice(eq + 1);
153
- for (const [pattern, name, hint] of GENERALIZE_KEY_PATTERNS) {
154
- if (pattern.test(key) && !seen.has(name)) {
155
- seen.add(name);
156
- vars.push({ placeholder: `<${name}>`, original: val, hint });
157
- return `${key}=<${name}>`;
158
- }
159
- }
160
- if (/id$/iu.test(key) && (UUID_RE.test(val) || HEX32_RE.test(val))) {
161
- const name = key.replace(/^--(?:cli-)?/u, "").replace(/[_=]/gu, "-");
162
- if (!seen.has(name)) {
163
- seen.add(name);
164
- vars.push({ placeholder: `<${name}>`, original: val, hint: "执行前通过只读查询获取" });
165
- return `${key}=<${name}>`;
166
- }
167
- }
168
- }
169
- if ((UUID_RE.test(arg) || HEX32_RE.test(arg)) && !seen.has("resource-id")) {
170
- seen.add("resource-id");
171
- vars.push({ placeholder: "<resource-id>", original: arg, hint: "执行前通过只读查询获取" });
172
- return "<resource-id>";
173
- }
174
- return arg;
175
- });
176
- return { args: result, vars };
177
- }
178
-
179
- export function renderCloudPromptTemplate(state: WorkflowState, notes = ""): string {
180
- if (state.mode !== "cloud" || !state.details) throw new Error("Cloud workflow state is required");
181
- const details = state.details;
182
- if (details.successfulSteps.length === 0) {
183
- throw new Error("No successful Cloud execution steps are available to summarize");
184
- }
185
- const provider = getCloudProvider(details.vendor);
186
- const allVars: GeneralizedVar[] = [];
187
- const steps = details.successfulSteps.map((step, index) => {
188
- const gen = generalizeArgs(step.args);
189
- for (const v of gen.vars) {
190
- if (!allVars.some((e) => e.placeholder === v.placeholder)) allVars.push(v);
191
- }
192
- return `${index + 1}. [${step.operation}] ${singleLine(step.intent)}\n`
193
- + ` - Validated approach: ${singleLine(step.approach)}\n`
194
- + ` - Known-good command: \`${inlineCode([step.command, ...gen.args].join(" "))}\``;
195
- }).join("\n");
196
- const failedPaths = details.failedApproaches.length > 0
197
- ? details.failedApproaches.map((failure, index) => (
198
- `${index + 1}. **DO NOT RETRY:** ${singleLine(failure.approach)}\n`
199
- + ` - Recorded failure: ${singleLine(failure.reason)}`
200
- )).join("\n")
201
- : "No failed execution paths were recorded for this run.";
202
- return [
203
- "Execute this HWCode Cloud task from a layered record of a previously successful run.",
204
- "",
205
- "## Layer 1 — Stable intent and boundaries",
206
- "",
207
- `Preferred provider: ${provider.label}`,
208
- `Deploy current project: ${details.deployCurrentProject ? "yes" : "no"}`,
209
- `Objective pattern: ${details.request}`,
210
- "",
211
- "Preserve the objective, provider boundary, credential isolation, resource approvals, and deletion confirmations. Adapt only environment-specific values.",
212
- "",
213
- "## Layer 2 — Preconditions and operator knowledge",
214
- "",
215
- "Start with read-only discovery. Re-check the active account, region, project, resource state, tool versions, and workspace paths before making changes.",
216
- ...(notes ? ["", "Validated prerequisites, lessons, and expensive pitfalls:", notes] : []),
217
- ...(allVars.length > 0 ? [
218
- "",
219
- "Environment-specific values (resolve via read-only discovery before executing):",
220
- ...allVars.map((v) => ` ${v.placeholder.padEnd(20)} 原值: ${v.original} → ${v.hint}`),
221
- ] : []),
222
- "",
223
- "## Layer 3 — Preferred validated execution path",
224
- "",
225
- "Use this path first and preserve its order. Replace <placeholder> values with discovered IDs from the current environment before executing. The command structure is the known-good reference; do not replace it with speculative alternatives.",
226
- "",
227
- steps,
228
- "",
229
- "## Layer 4 — Expensive failed paths to avoid",
230
- "",
231
- "Do not retry any path below, including renamed or parameter-only variants. Reconsider one only when the user explicitly directs it after reviewing the recorded failure.",
232
- "",
233
- failedPaths,
234
- "",
235
- "## Layer 5 — Execution contract",
236
- "",
237
- "Present the adapted plan and obtain all normal HWCode Cloud approvals. Never copy credentials, assume old resources still exist, or skip current-state verification.",
238
- "If HWCode Cloud is not active, do not run cloud commands directly; ask the user to start this template through /hwcode-cloud-template.",
239
- "",
240
- "Additional instructions: ${@:-Use the objective and successful sequence above.}",
241
- ].join("\n");
242
- }
243
-
244
- function renderTemplateFile(
245
- name: string,
246
- state: WorkflowState,
247
- notes: string,
248
- createdAt: string,
249
- updatedAt: string,
250
- ): string {
251
- if (state.mode !== "cloud" || !state.details) throw new Error("Cloud workflow state is required");
252
- const body = renderCloudPromptTemplate(state, notes);
253
- return [
254
- "---",
255
- `description: ${JSON.stringify(name)}`,
256
- 'argument-hint: "[additional instructions]"',
257
- `hwcode-cloud-name: ${JSON.stringify(name)}`,
258
- `hwcode-cloud-created-at: ${JSON.stringify(createdAt)}`,
259
- `hwcode-cloud-updated-at: ${JSON.stringify(updatedAt)}`,
260
- `hwcode-cloud-vendor: ${JSON.stringify(state.details.vendor)}`,
261
- `hwcode-cloud-deploy: ${state.details.deployCurrentProject}`,
262
- `hwcode-cloud-objective: ${JSON.stringify(state.details.request)}`,
263
- `hwcode-cloud-notes: ${JSON.stringify(notes)}`,
264
- "---",
265
- body,
266
- "",
267
- ].join("\n");
268
- }
269
-
270
- function writeTemplateFile(path: string, content: string): void {
271
- const directory = dirname(path);
272
- mkdirSync(directory, { recursive: true, mode: 0o700 });
273
- chmodSync(directory, 0o700);
274
- const temporaryPath = `${path}.${process.pid}.tmp`;
275
- writeFileSync(temporaryPath, content, { encoding: "utf8", mode: 0o600 });
276
- chmodSync(temporaryPath, 0o600);
277
- renameSync(temporaryPath, path);
278
- chmodSync(path, 0o600);
279
- }
280
-
281
- export function saveCloudPromptTemplate(
282
- requestedName: string,
283
- state: WorkflowState,
284
- directory = defaultCloudPromptDirectory(),
285
- notes = "",
286
- savedAt = new Date(),
287
- ): CloudPromptTemplate {
288
- if (state.mode !== "cloud" || !state.details) throw new Error("Cloud workflow state is required");
289
- const name = cleanTemplateName(requestedName);
290
- if (!name) throw new Error("Cloud Prompt Template name is required");
291
- const idBase = `hwcloud-${slugify(name)}`;
292
- const timestamp = savedAt.toISOString();
293
- const content = renderTemplateFile(name, state, notes, timestamp, timestamp);
294
- let id = idBase;
295
- let path = join(directory, `${id}.md`);
296
- for (let suffix = 2; existsSync(path); suffix++) {
297
- id = `${idBase}-${suffix}`;
298
- path = join(directory, `${id}.md`);
299
- }
300
- writeTemplateFile(path, content);
301
- return parseCloudPromptTemplate(path)!;
302
- }
303
-
304
- export function updateCloudPromptTemplate(
305
- template: CloudPromptTemplate,
306
- state: WorkflowState,
307
- notes = template.notes,
308
- updatedAt = new Date(),
309
- ): CloudPromptTemplate {
310
- const timestamp = updatedAt.toISOString();
311
- const content = renderTemplateFile(template.name, state, notes, template.createdAt, timestamp);
312
- writeTemplateFile(template.path, content);
313
- return parseCloudPromptTemplate(template.path)!;
314
- }