@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
@@ -5,6 +5,7 @@ import {
5
5
  filterCommandSuggestions,
6
6
  parseHiddenCommands,
7
7
  } from "../lib/command-filter.ts";
8
+ import { notify } from "../lib/extension-ui.ts";
8
9
  import { loadLayeredJson } from "../lib/runtime/config.ts";
9
10
 
10
11
  function loadHiddenCommands(cwd: string, notify: (message: string) => void): Set<string> {
@@ -21,9 +22,7 @@ function loadHiddenCommands(cwd: string, notify: (message: string) => void): Set
21
22
  export default function commandFilterExtension(pi: ExtensionAPI) {
22
23
  pi.on("session_start", async (_event, ctx) => {
23
24
  if (ctx.mode !== "tui") return;
24
- const hiddenCommands = loadHiddenCommands(ctx.cwd, (message) => {
25
- if (ctx.hasUI) ctx.ui.notify(message, "warning");
26
- });
25
+ const hiddenCommands = loadHiddenCommands(ctx.cwd, (message) => notify(ctx, message, "warning"));
27
26
 
28
27
  ctx.ui.addAutocompleteProvider((current) => ({
29
28
  triggerCharacters: current.triggerCharacters,
@@ -5,6 +5,7 @@ import type {
5
5
  } from "@earendil-works/pi-coding-agent";
6
6
  import { createLocalBashOperations } from "@earendil-works/pi-coding-agent";
7
7
 
8
+ import { notify } from "../lib/extension-ui.ts";
8
9
  import {
9
10
  canonicalizeDirectory,
10
11
  clearWorkingDirectoryState,
@@ -28,10 +29,6 @@ interface ChangeResult {
28
29
  error?: string;
29
30
  }
30
31
 
31
- function notify(ctx: ExtensionContext, message: string, level: "info" | "warning" | "error" = "info"): void {
32
- if (ctx.hasUI) ctx.ui.notify(message, level);
33
- }
34
-
35
32
  function commandArgument(args: string): string {
36
33
  const trimmed = args.trim();
37
34
  if (!trimmed) return "";
@@ -0,0 +1,224 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { dirname } from "node:path";
3
+ import { Worker } from "node:worker_threads";
4
+
5
+ import { Type } from "@earendil-works/pi-ai";
6
+ import { defineTool, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
7
+
8
+ import {
9
+ buildKnowledgeExtractionPrompt, KNOWLEDGE_EXTRACTION_SYSTEM_PROMPT,
10
+ } from "../lib/knowledge/extractor.ts";
11
+ import { matchKnowledge } from "../lib/knowledge/matcher.ts";
12
+ import { loadKnowledgeById, loadKnowledgeSnapshot, projectKnowledgeKey } from "../lib/knowledge/store.ts";
13
+ import type { KnowledgeWorkerInput, KnowledgeWorkerOutput } from "../lib/knowledge/worker-protocol.ts";
14
+ import { KNOWLEDGE_RUNTIME_DEFAULTS } from "../lib/runtime/defaults.ts";
15
+ import { getWorkingDirectory } from "../lib/working-directory.ts";
16
+
17
+ interface ActiveReview {
18
+ requestId: string;
19
+ leaderToken: string;
20
+ controller: AbortController;
21
+ }
22
+
23
+ interface ProcessKnowledgeRuntime {
24
+ worker?: Worker;
25
+ context?: ExtensionContext;
26
+ sessionsRoot?: string;
27
+ modelAvailable?: boolean;
28
+ activeReview?: ActiveReview;
29
+ capabilityTimer?: NodeJS.Timeout;
30
+ }
31
+
32
+ const RUNTIME_SYMBOL = Symbol.for("hwcode.knowledge.runtime.v3");
33
+ const processGlobals = globalThis as unknown as Record<PropertyKey, unknown>;
34
+ const runtime = (processGlobals[RUNTIME_SYMBOL] ??= {}) as ProcessKnowledgeRuntime;
35
+
36
+ function post(message: KnowledgeWorkerInput): void {
37
+ runtime.worker?.postMessage(message);
38
+ }
39
+
40
+ function modelAvailable(ctx: ExtensionContext | undefined): boolean {
41
+ return Boolean(ctx?.model && ctx.modelRegistry.hasConfiguredAuth(ctx.model));
42
+ }
43
+
44
+ function updateCapability(): void {
45
+ if (!runtime.worker && runtime.context) ensureWorker();
46
+ if (!runtime.worker || !runtime.sessionsRoot) return;
47
+ const available = modelAvailable(runtime.context);
48
+ if (runtime.modelAvailable === available) return;
49
+ runtime.modelAvailable = available;
50
+ post({ type: "configure", modelAvailable: available, sessionsRoot: runtime.sessionsRoot });
51
+ }
52
+
53
+ function cancelActiveReview(): void {
54
+ runtime.activeReview?.controller.abort();
55
+ }
56
+
57
+ async function runReview(message: Extract<KnowledgeWorkerOutput, { type: "review_request" }>): Promise<void> {
58
+ const ctx = runtime.context;
59
+ if (!ctx?.model || !ctx.modelRegistry.hasConfiguredAuth(ctx.model)) {
60
+ post({ type: "review_result", leaderToken: message.leaderToken, requestId: message.requestId, error: "no-configured-model" });
61
+ updateCapability();
62
+ return;
63
+ }
64
+ if (!ctx.isIdle() || ctx.hasPendingMessages()) {
65
+ post({ type: "review_result", leaderToken: message.leaderToken, requestId: message.requestId, error: "model-executor-is-busy" });
66
+ return;
67
+ }
68
+ const controller = new AbortController();
69
+ runtime.activeReview = { requestId: message.requestId, leaderToken: message.leaderToken, controller };
70
+ const timeout = setTimeout(() => controller.abort(), KNOWLEDGE_RUNTIME_DEFAULTS.review.modelTimeoutMs);
71
+ timeout.unref();
72
+ try {
73
+ const response = await ctx.modelRegistry.complete(
74
+ ctx.model,
75
+ {
76
+ systemPrompt: KNOWLEDGE_EXTRACTION_SYSTEM_PROMPT,
77
+ messages: [{
78
+ role: "user",
79
+ content: [{ type: "text", text: buildKnowledgeExtractionPrompt(message.task.projectRoot, message.task.delta) }],
80
+ timestamp: Date.now(),
81
+ }],
82
+ },
83
+ { signal: controller.signal, reasoningEffort: "low", cacheRetention: "none", sessionId: randomUUID() },
84
+ );
85
+ if (controller.signal.aborted) throw new Error("knowledge-review-aborted-or-timed-out");
86
+ const raw = response.content.filter((item): item is { type: "text"; text: string } => item.type === "text")
87
+ .map((item) => item.text).join("\n");
88
+ post({ type: "review_result", leaderToken: message.leaderToken, requestId: message.requestId, raw });
89
+ } catch (error) {
90
+ post({
91
+ type: "review_result", leaderToken: message.leaderToken, requestId: message.requestId,
92
+ error: error instanceof Error ? error.message : String(error),
93
+ });
94
+ } finally {
95
+ clearTimeout(timeout);
96
+ if (runtime.activeReview?.requestId === message.requestId) runtime.activeReview = undefined;
97
+ }
98
+ }
99
+
100
+ function handleWorkerMessage(message: KnowledgeWorkerOutput): void {
101
+ if (message.type === "review_request") { void runReview(message); return; }
102
+ if (message.type === "review_cancel" && runtime.activeReview?.requestId === message.requestId) {
103
+ runtime.activeReview.controller.abort();
104
+ }
105
+ }
106
+
107
+ function ensureWorker(): Worker {
108
+ if (runtime.worker) return runtime.worker;
109
+ const worker = new Worker(new URL("../lib/knowledge/review-worker.ts", import.meta.url));
110
+ worker.unref();
111
+ runtime.worker = worker;
112
+ worker.on("message", (message: KnowledgeWorkerOutput) => handleWorkerMessage(message));
113
+ worker.on("error", () => {
114
+ cancelActiveReview();
115
+ if (runtime.worker === worker) {
116
+ runtime.worker = undefined;
117
+ runtime.modelAvailable = undefined;
118
+ }
119
+ });
120
+ worker.on("exit", () => {
121
+ if (runtime.worker === worker) {
122
+ runtime.worker = undefined;
123
+ runtime.modelAvailable = undefined;
124
+ }
125
+ });
126
+ runtime.capabilityTimer ??= setInterval(updateCapability, KNOWLEDGE_RUNTIME_DEFAULTS.review.capabilityPollMs);
127
+ runtime.capabilityTimer.unref();
128
+ return worker;
129
+ }
130
+
131
+ function configureForContext(ctx: ExtensionContext): void {
132
+ runtime.context = ctx;
133
+ runtime.sessionsRoot = dirname(ctx.sessionManager.getSessionDir());
134
+ ensureWorker();
135
+ const available = modelAvailable(ctx);
136
+ runtime.modelAvailable = available;
137
+ post({ type: "configure", modelAvailable: available, sessionsRoot: runtime.sessionsRoot });
138
+ }
139
+
140
+ function currentProject(ctx: ExtensionContext): { root: string; key: string } {
141
+ const root = getWorkingDirectory(ctx.sessionManager);
142
+ return { root, key: projectKnowledgeKey(root) };
143
+ }
144
+
145
+ export default function registerKnowledgeExtension(pi: ExtensionAPI): void {
146
+ pi.registerTool(defineTool({
147
+ name: "hwcode_knowledge_lookup",
148
+ label: "HWCode Knowledge Lookup",
149
+ description: "Search the shared HWCode knowledge catalog and load a detailed topic by ID.",
150
+ promptSnippet: "Use the compact knowledge index, then load a detailed topic only when it is relevant.",
151
+ parameters: Type.Object({
152
+ id: Type.Optional(Type.String({ description: "Canonical topic ID from the loaded HWCode knowledge index" })),
153
+ query: Type.Optional(Type.String({ description: "Keywords to search in the complete local knowledge catalog" })),
154
+ }),
155
+ executionMode: "sequential",
156
+ async execute(_id, params) {
157
+ const ctx = runtime.context;
158
+ if (!ctx) return { content: [{ type: "text", text: "Knowledge context is not available." }], isError: true, details: {} };
159
+ const { key } = currentProject(ctx);
160
+ if (params.id) {
161
+ const found = loadKnowledgeById(params.id, key);
162
+ if (!found) return { content: [{ type: "text", text: `No applicable knowledge found with ID: ${params.id}` }], isError: true, details: {} };
163
+ return { content: [{ type: "text", text: found.content }], details: {} };
164
+ }
165
+ if (params.query) {
166
+ const snapshot = loadKnowledgeSnapshot(key);
167
+ const matches = matchKnowledge(params.query, snapshot.catalog)
168
+ .filter((item) => item.scope === "global" || item.scope === `project:${key}`);
169
+ if (matches.length === 0) return { content: [{ type: "text", text: `No relevant knowledge found for: ${params.query}` }], details: {} };
170
+ return {
171
+ content: [{ type: "text", text: matches.map((item) => `- [${item.id}] ${item.title}: ${item.summary}`).join("\n") }],
172
+ details: {},
173
+ };
174
+ }
175
+ return { content: [{ type: "text", text: "Provide either id or query." }], isError: true, details: {} };
176
+ },
177
+ }));
178
+
179
+ pi.on("session_start", (_event, ctx) => configureForContext(ctx));
180
+
181
+ pi.on("model_select", (_event, ctx) => {
182
+ runtime.context = ctx;
183
+ updateCapability();
184
+ });
185
+
186
+ pi.on("before_agent_start", (event, ctx) => {
187
+ runtime.context = ctx;
188
+ updateCapability();
189
+ const { key } = currentProject(ctx);
190
+ const snapshot = loadKnowledgeSnapshot(key);
191
+ const sections = [
192
+ snapshot.rulesPrompt ? `<hwcode_rules>\n${snapshot.rulesPrompt}\n</hwcode_rules>` : "",
193
+ snapshot.memoryPrompt ? `<hwcode_knowledge_index>\n${snapshot.memoryPrompt}\n</hwcode_knowledge_index>` : "",
194
+ ].filter(Boolean);
195
+ if (sections.length === 0) return undefined;
196
+ return { systemPrompt: `${event.systemPrompt}\n\n${sections.join("\n\n")}` };
197
+ });
198
+
199
+ pi.on("input", (_event, ctx) => {
200
+ runtime.context = ctx;
201
+ cancelActiveReview();
202
+ updateCapability();
203
+ return undefined;
204
+ });
205
+
206
+ pi.on("agent_start", (_event, ctx) => {
207
+ runtime.context = ctx;
208
+ cancelActiveReview();
209
+ });
210
+
211
+ pi.on("agent_settled", (_event, ctx) => {
212
+ runtime.context = ctx;
213
+ updateCapability();
214
+ post({ type: "scan_now" });
215
+ });
216
+
217
+ pi.on("session_shutdown", (event) => {
218
+ cancelActiveReview();
219
+ if (event.reason === "quit") {
220
+ runtime.context = undefined;
221
+ updateCapability();
222
+ }
223
+ });
224
+ }
@@ -4,29 +4,29 @@ import { resolve } from "node:path";
4
4
 
5
5
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
6
6
 
7
+ import { notify } from "../../../lib/extension-ui.ts";
7
8
  import { modelConfigurationIssue } from "../../../lib/models/readiness.ts";
8
9
  import { CLOUD_RUNTIME_DEFAULTS } from "../../../lib/runtime/defaults.ts";
9
10
  import { remoteRunnerPaths, userRuntimePaths } from "../../../lib/runtime/paths.ts";
10
11
  import { canonicalizeWorkspaceRoot } from "../../../lib/workflow-guard.ts";
11
12
  import { getWorkingDirectory } from "../../../lib/working-directory.ts";
12
13
  import { checkProviderCli, formatValidationFailure, validateCloudCredentials } from "../../../lib/workflows/cloud/adapters.ts";
13
- import { cloudDeploymentTemplateSource, listCloudDeploymentTemplates, materializeCloudDeploymentTemplate, type CloudDeploymentTemplate } from "../../../lib/workflows/cloud/bundles.ts";
14
+ import { cloudTerraformTemplateSource, listCloudTerraformTemplates, materializeCloudTerraformTemplate, type CloudTerraformTemplate } from "../../../lib/workflows/cloud/bundles.ts";
14
15
  import { CLOUD_PROVIDERS, getCloudProvider, inaccessibleCloudCliMessage, missingCloudCliMessage, type CloudCredentials, type CloudVendorId } from "../../../lib/workflows/cloud/providers.ts";
15
16
  import { connectRemoteTarget } from "../../../lib/workflows/cloud/remote/connect.ts";
16
17
  import { remoteTargetLabel, remoteTargetSummary, type RemoteTargetProfile } from "../../../lib/workflows/cloud/remote/profiles.ts";
17
- import { cloudPromptTemplateSource, expandCloudPromptTemplate, listCloudPromptTemplates, type CloudPromptTemplate } from "../../../lib/workflows/cloud/templates.ts";
18
18
  import {
19
19
  cloudCredentialProfileLabel, defaultCloudVaultPath, listCloudCredentialProfiles,
20
20
  listRemoteTargetProfiles, saveCloudCredentialProfile, saveRemoteTargetProfile,
21
21
  writeCloudVault, type CloudCredentialProfile,
22
22
  } from "../../../lib/workflows/cloud/vault.ts";
23
- import { createCloudRunWorkspace } from "../../../lib/workflows/cloud/workspace.ts";
24
- import { activeWorkflow, createCloudWorkflowState, type CloudWorkflowDetails, type WorkflowState } from "../../../lib/workflows/state.ts";
25
- import type { CloudExtensionRuntime } from "./runtime.ts";
23
+ import { cloudArtifactDirectory, createCloudRunWorkspace } from "../../../lib/workflows/cloud/workspace.ts";
26
24
  import {
27
- activationPrompt, cloudDetails, collectCredentials, formatTemplateTime, notify,
28
- remoteTrustInteraction, restoreCloudWorkflow, restoreLegacyCloudWorkflow, unlockVault,
29
- } from "./shared.ts";
25
+ activeWorkflow, cloudDetails, createCloudWorkflowState,
26
+ type CloudWorkflowDetails, type WorkflowState,
27
+ } from "../../../lib/workflows/state.ts";
28
+ import { collectCredentials, remoteTrustInteraction, unlockVault } from "./interactions.ts";
29
+ import type { CloudExtensionRuntime } from "./runtime.ts";
30
30
 
31
31
  type RunnerSelection =
32
32
  | { kind: "cancel" }
@@ -34,6 +34,34 @@ type RunnerSelection =
34
34
  | { kind: "deferred" }
35
35
  | { kind: "profile"; profile: RemoteTargetProfile };
36
36
 
37
+ function formatTemplateTime(value: string): string {
38
+ const date = new Date(value);
39
+ if (Number.isNaN(date.valueOf())) return value;
40
+ const pad = (part: number) => String(part).padStart(2, "0");
41
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
42
+ }
43
+
44
+ function activationPrompt(state: WorkflowState): string {
45
+ const details = cloudDetails(state)!;
46
+ const provider = getCloudProvider(details.vendor);
47
+ const guidance = details.templateGuidance
48
+ ? `\n\nReusable template guidance (${details.sourceTemplate?.name ?? "saved template"}):\n${details.templateGuidance}`
49
+ : "";
50
+ const runner = details.runner?.name
51
+ ?? (details.runnerPreference === "automatic" ? "automatic temporary Runner requested" : "not selected");
52
+ return `/skill:hwcode-cloud HWCode Cloud workflow activated.
53
+
54
+ Locked project root: ${state.root}
55
+ Task artifact workspace: ${cloudArtifactDirectory(state)}
56
+ Cloud provider: ${getCloudProvider(details.vendor).label}
57
+ Terraform Runner: ${runner}(Note: Runner is only for Terraform; regular discovery and CLI queries run locally via ${provider.cli})
58
+ Deploy current project: ${details.deployCurrentProject ? "yes" : "no"}
59
+ User objective:\n${details.request}${guidance}
60
+
61
+ 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.
62
+ `;
63
+ }
64
+
37
65
  async function chooseRunner(ctx: ExtensionCommandContext, payload: ReturnType<typeof import("../../../lib/workflows/cloud/vault.ts").createEmptyVault>, vendor: CloudVendorId, root: string, region: string): Promise<RunnerSelection> {
38
66
  const existing = listRemoteTargetProfiles(payload).filter((profile) => profile.vendor === vendor);
39
67
  const labels = existing.map(remoteTargetLabel);
@@ -81,25 +109,19 @@ async function ensureConversationModel(ctx: ExtensionCommandContext): Promise<bo
81
109
  return false;
82
110
  }
83
111
 
84
- export async function choosePromptTemplate(ctx: ExtensionCommandContext): Promise<CloudPromptTemplate | undefined> {
85
- const templates = listCloudPromptTemplates();
86
- if (templates.length === 0) { notify(ctx, "还没有本地 Cloud Prompt Template。成功执行任务后使用 /hwcode-cloud-save-template 保存。", "warning"); return undefined; }
87
- const labels = templates.map((template) => `${template.name} · ${formatTemplateTime(template.updatedAt)}`);
88
- const selected = await ctx.ui.select("选择已验证的 Cloud Prompt Template", labels);
89
- const index = selected ? labels.indexOf(selected) : -1;
90
- return index >= 0 ? templates[index] : undefined;
91
- }
92
-
93
- export async function chooseDeploymentTemplate(ctx: ExtensionCommandContext): Promise<CloudDeploymentTemplate | undefined> {
94
- const templates = listCloudDeploymentTemplates();
95
- if (templates.length === 0) { notify(ctx, "还没有 Terraform Deployment Template。成功 apply 后使用 /hwcode-cloud-save-template 保存。", "warning"); return undefined; }
112
+ export async function chooseTerraformTemplate(ctx: ExtensionCommandContext): Promise<CloudTerraformTemplate | undefined> {
113
+ const templates = listCloudTerraformTemplates();
114
+ if (templates.length === 0) {
115
+ notify(ctx, "还没有 Terraform Template。首次成功完成 managed apply 后会自动保存。", "warning");
116
+ return undefined;
117
+ }
96
118
  const labels = templates.map((template) => `${template.manifest.name} · ${template.manifest.vendor} · ${formatTemplateTime(template.manifest.updatedAt)}`);
97
- const selected = await ctx.ui.select("选择 Terraform Deployment Template", labels);
119
+ const selected = await ctx.ui.select("选择 Terraform Template", labels);
98
120
  const index = selected ? labels.indexOf(selected) : -1;
99
121
  return index >= 0 ? templates[index] : undefined;
100
122
  }
101
123
 
102
- export async function activateCloudWorkflow(runtime: CloudExtensionRuntime, args: string, ctx: ExtensionCommandContext, initialTemplate?: CloudPromptTemplate, initialBundle?: CloudDeploymentTemplate): Promise<void> {
124
+ export async function activateCloudWorkflow(runtime: CloudExtensionRuntime, args: string, ctx: ExtensionCommandContext, initialBundle?: CloudTerraformTemplate): Promise<void> {
103
125
  if (ctx.mode !== "tui") { notify(ctx, "HWCode Cloud 要求在交互式 TUI 中启动,以安全遮罩凭据输入。", "error"); return; }
104
126
  if (!ctx.isIdle()) { notify(ctx, "请等待当前响应完成后再启动 HWCode Cloud。", "warning"); return; }
105
127
  if (!(await ensureConversationModel(ctx))) return;
@@ -110,7 +132,7 @@ export async function activateCloudWorkflow(runtime: CloudExtensionRuntime, args
110
132
  notify(ctx, `当前会话已有 ${currentWorkflow.mode} workflow。请新建 session 后再启动 HWCode Cloud。`, "warning");
111
133
  return;
112
134
  }
113
- const storedState = restoreCloudWorkflow(ctx) ?? restoreLegacyCloudWorkflow(ctx);
135
+ const storedState = runtime.restore(ctx);
114
136
  let resumedState: WorkflowState | undefined;
115
137
  if (storedState && storedState.root === root && !cloudDetails(storedState)?.terminalFailure) {
116
138
  const storedDetails = cloudDetails(storedState)!;
@@ -122,14 +144,11 @@ export async function activateCloudWorkflow(runtime: CloudExtensionRuntime, args
122
144
  if (choice.startsWith("恢复 ")) resumedState = storedState;
123
145
  }
124
146
 
125
- let template = initialTemplate;
126
- if (!resumedState && !template && !initialBundle && !args.trim() && (listCloudPromptTemplates().length > 0 || listCloudDeploymentTemplates().length > 0)) {
127
- const source = await ctx.ui.select("Cloud 任务来源", ["新建任务", "使用已有 Prompt Template", "使用 Terraform Deployment Template", "取消"]
128
- .filter((item) => item !== "使用已有 Prompt Template" || listCloudPromptTemplates().length > 0)
129
- .filter((item) => item !== "使用 Terraform Deployment Template" || listCloudDeploymentTemplates().length > 0));
147
+ let bundle = initialBundle;
148
+ if (!resumedState && !bundle && !args.trim() && listCloudTerraformTemplates().length > 0) {
149
+ const source = await ctx.ui.select("Cloud 任务来源", ["新建任务", "使用 Terraform Template", "取消"]);
130
150
  if (!source || source === "取消") return;
131
- if (source === "使用已有 Prompt Template") { template = await choosePromptTemplate(ctx); if (!template) return; }
132
- if (source === "使用 Terraform Deployment Template") { initialBundle = await chooseDeploymentTemplate(ctx); if (!initialBundle) return; }
151
+ if (source === "使用 Terraform Template") { bundle = await chooseTerraformTemplate(ctx); if (!bundle) return; }
133
152
  }
134
153
 
135
154
  let vendor: CloudVendorId;
@@ -139,16 +158,11 @@ export async function activateCloudWorkflow(runtime: CloudExtensionRuntime, args
139
158
  if (resumedState) {
140
159
  const details = cloudDetails(resumedState)!;
141
160
  ({ vendor, deployCurrentProject, request } = details);
142
- } else if (initialBundle) {
143
- vendor = initialBundle.manifest.vendor;
161
+ } else if (bundle) {
162
+ vendor = bundle.manifest.vendor;
144
163
  deployCurrentProject = true;
145
- request = `复用 Terraform Deployment Template「${initialBundle.manifest.name}」并完成其验证、计划与部署`;
146
- templateGuidance = existsSync(initialBundle.summaryPath) ? readFileSync(initialBundle.summaryPath, "utf8") : `sourceDigest=${initialBundle.manifest.sourceDigest}`;
147
- } else if (template) {
148
- vendor = template.vendor;
149
- deployCurrentProject = template.deployCurrentProject;
150
- request = template.objective || template.name;
151
- templateGuidance = expandCloudPromptTemplate(template, args);
164
+ request = `复用 Terraform Template「${bundle.manifest.name}」并完成其验证、计划与部署`;
165
+ templateGuidance = existsSync(bundle.summaryPath) ? readFileSync(bundle.summaryPath, "utf8") : `sourceDigest=${bundle.manifest.sourceDigest}`;
152
166
  } else {
153
167
  const label = await ctx.ui.select("选择需要对接的云计算厂商", CLOUD_PROVIDERS.map((provider) => provider.label));
154
168
  if (!label) return;
@@ -237,16 +251,16 @@ export async function activateCloudWorkflow(runtime: CloudExtensionRuntime, args
237
251
  runtime.cleanupCredentialDirectories();
238
252
  runtime.activeCredentials = credentials;
239
253
  const artifactWorkspace = resumedState ? undefined : createCloudRunWorkspace(root);
240
- const materializedBundle = initialBundle && artifactWorkspace ? materializeCloudDeploymentTemplate(initialBundle, artifactWorkspace.path) : undefined;
254
+ const materializedBundle = bundle && artifactWorkspace ? materializeCloudTerraformTemplate(bundle, artifactWorkspace.path) : undefined;
241
255
  runtime.activeState = resumedState ?? createCloudWorkflowState(
242
256
  root, vendor, deployCurrentProject, request,
243
- (template || initialBundle) && templateGuidance ? {
244
- source: initialBundle ? cloudDeploymentTemplateSource(initialBundle) : cloudPromptTemplateSource(template!),
257
+ bundle && templateGuidance ? {
258
+ source: cloudTerraformTemplateSource(bundle),
245
259
  guidance: templateGuidance,
246
260
  } : undefined,
247
261
  artifactWorkspace?.path,
248
262
  );
249
- if (initialBundle && !resumedState) runtime.activeState = runtime.replaceDetails(runtime.activeState, { ...cloudDetails(runtime.activeState)!, terraformSourcePath: materializedBundle!.terraformPath }, runtime.activeState.phase);
263
+ if (bundle && !resumedState) runtime.activeState = runtime.replaceDetails(runtime.activeState, { ...cloudDetails(runtime.activeState)!, terraformSourcePath: materializedBundle!.terraformPath }, runtime.activeState.phase);
250
264
  if (runtime.activeRunner) runtime.activeState = runtime.replaceDetails(runtime.activeState, { ...cloudDetails(runtime.activeState)!, runnerPreference, runner: remoteTargetSummary(runtime.activeRunner) }, runtime.activeState.phase);
251
265
  else runtime.activeState = runtime.replaceDetails(runtime.activeState, { ...cloudDetails(runtime.activeState)!, runnerPreference }, runtime.activeState.phase);
252
266
  notify(ctx, `${vendorLabel} 连接成功。凭据已加密保存,项目根目录锁定为 ${root}。`);
@@ -1,96 +1,11 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
 
3
- import { cloudDeploymentTemplateSource, listCloudDeploymentTemplates, saveCloudDeploymentTemplate, updateCloudDeploymentTemplate } from "../../../lib/workflows/cloud/bundles.ts";
4
- import { redactCredentialValues } from "../../../lib/workflows/cloud/providers.ts";
5
- import { saveCloudWorkflowTemplate } from "../../../lib/workflows/cloud/template-save.ts";
6
- import { listCloudPromptTemplates, saveCloudPromptTemplate, updateCloudPromptTemplate } from "../../../lib/workflows/cloud/templates.ts";
7
- import { activateCloudWorkflow, chooseDeploymentTemplate, choosePromptTemplate } from "./activation.ts";
3
+ import { activateCloudWorkflow } from "./activation.ts";
8
4
  import type { CloudExtensionRuntime } from "./runtime.ts";
9
- import { cloudDetails, formatTemplateTime, notify } from "./shared.ts";
10
5
 
11
6
  export function registerCloudCommands(pi: ExtensionAPI, runtime: CloudExtensionRuntime): void {
12
7
  pi.registerCommand("hwcode-cloud", {
13
8
  description: "Start or resume the guarded HWCode cloud workflow",
14
9
  handler: (args, ctx) => activateCloudWorkflow(runtime, args, ctx),
15
10
  });
16
-
17
- pi.registerCommand("hwcode-cloud-template", {
18
- description: "Start HWCode Cloud from a locally saved prompt or Terraform deployment template",
19
- handler: async (args, ctx) => {
20
- const promptTemplates = listCloudPromptTemplates();
21
- const deploymentTemplates = listCloudDeploymentTemplates();
22
- const source = await ctx.ui.select("选择本地 Cloud Template 类型", [
23
- ...(promptTemplates.length > 0 ? ["Prompt Template"] : []),
24
- ...(deploymentTemplates.length > 0 ? ["Terraform Deployment Template"] : []),
25
- "取消",
26
- ]);
27
- if (source === "Prompt Template") {
28
- const template = await choosePromptTemplate(ctx);
29
- if (template) await activateCloudWorkflow(runtime, args, ctx, template);
30
- } else if (source === "Terraform Deployment Template") {
31
- const bundle = await chooseDeploymentTemplate(ctx);
32
- if (bundle) await activateCloudWorkflow(runtime, args, ctx, undefined, bundle);
33
- }
34
- },
35
- });
36
-
37
- pi.registerCommand("hwcode-cloud-save-template", {
38
- description: "Save or update the successful Cloud path as a Prompt or Terraform Deployment Template",
39
- handler: async (args, ctx) => {
40
- const activeState = runtime.restore(ctx);
41
- const details = activeState && cloudDetails(activeState);
42
- if (!activeState || !details || (details.successfulSteps.length === 0 && details.terraformRun?.phase !== "applied")) {
43
- notify(ctx, "当前 Cloud workflow 还没有成功执行步骤,无法生成可复用模板。", "warning");
44
- return;
45
- }
46
- if (details.terraformRun?.phase === "applied" && details.terraformRun.sourcePath) {
47
- const existing = details.sourceTemplate?.kind === "terraform"
48
- ? listCloudDeploymentTemplates().find((template) => template.manifest.id === details.sourceTemplate?.id)
49
- : undefined;
50
- let action: "update" | "create" = "create";
51
- if (existing) {
52
- const choice = await ctx.ui.select(`当前会话关联模板「${existing.manifest.name}」`, ["更新原模板", "创建新模板", "取消"]);
53
- if (!choice || choice === "取消") return;
54
- action = choice === "更新原模板" ? "update" : "create";
55
- }
56
- const name = (action === "update" ? existing!.manifest.name : args.trim() || await ctx.ui.input("Terraform Deployment Template 名称", `${details.vendor}-${new Date().toISOString().slice(0, 10)}`))?.trim();
57
- if (!name) return;
58
- try {
59
- const template = action === "update"
60
- ? updateCloudDeploymentTemplate(existing!, details.terraformRun.sourcePath, activeState, new Date(), details.artifactDirectory)
61
- : saveCloudDeploymentTemplate(name, details.terraformRun.sourcePath, activeState, undefined, new Date(), details.artifactDirectory);
62
- runtime.replaceDetails(activeState, { ...details, sourceTemplate: cloudDeploymentTemplateSource(template) });
63
- notify(ctx, `${action === "update" ? "已更新" : "已保存"} Terraform Deployment Template「${template.manifest.name}」,包含完整性清单、Terraform、Helm、脱敏 discovery/reports 与验证摘要。`);
64
- } catch (error) {
65
- notify(ctx, `Terraform Deployment Template 保存失败:${error instanceof Error ? error.message : String(error)}`, "error");
66
- }
67
- return;
68
- }
69
-
70
- const result = await saveCloudWorkflowTemplate({
71
- state: activeState,
72
- requestedName: args,
73
- redact: runtime.activeCredentials ? (value) => redactCredentialValues(value, runtime.activeCredentials!) : undefined,
74
- interaction: {
75
- async chooseSourceAction(source) {
76
- const updateLabel = `更新原模板「${source.name}」`;
77
- const choice = await ctx.ui.select(`当前会话关联模板「${details.sourceTemplate?.name ?? source.name}」`, [updateLabel, "创建新模板", "取消"]);
78
- if (!choice || choice === "取消") return "cancel";
79
- return choice === updateLabel ? "update" : "create";
80
- },
81
- inputName: (defaultName) => ctx.ui.input("模板名称", defaultName),
82
- editNotes: (initialNotes) => ctx.ui.editor("补充必要前置条件、成功经验及必须避免的高消耗错误路径(可留空;不得包含凭据)", initialNotes),
83
- sourceMissing(sourceName) { notify(ctx, `本次使用的原模板「${sourceName}」已不存在,将创建新模板。`, "warning"); },
84
- },
85
- store: {
86
- list: () => listCloudPromptTemplates(),
87
- create: (name, state, notes) => saveCloudPromptTemplate(name, state, undefined, notes),
88
- update: (template, state, notes) => updateCloudPromptTemplate(template, state, notes),
89
- },
90
- });
91
- if (result.status === "cancelled") return;
92
- runtime.replaceDetails(activeState, result.details);
93
- notify(ctx, `${result.action === "updated" ? "已更新" : "已创建"}模板「${result.template.name}」(${formatTemplateTime(result.template.updatedAt)})。使用 /hwcode-cloud-template 可立即复用。`);
94
- },
95
- });
96
11
  }
@@ -1,20 +1,15 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
 
3
+ import { notify } from "../../../lib/extension-ui.ts";
3
4
  import { CLOUD_RUNTIME_DEFAULTS } from "../../../lib/runtime/defaults.ts";
4
- import { containsCloudCommand, getCloudProvider } from "../../../lib/workflows/cloud/providers.ts";
5
+ import { getCloudProvider } from "../../../lib/workflows/cloud/providers.ts";
6
+ import { cloudArtifactDirectory } from "../../../lib/workflows/cloud/workspace.ts";
7
+ import { cloudDetails } from "../../../lib/workflows/state.ts";
5
8
  import type { CloudExtensionRuntime } from "./runtime.ts";
6
- import {
7
- appendWorkflowState, cloudArtifactDirectory, cloudDetails, notify,
8
- restoreLegacyCloudWorkflow,
9
- } from "./shared.ts";
10
9
 
11
10
  export function registerCloudEvents(pi: ExtensionAPI, runtime: CloudExtensionRuntime): void {
12
11
  pi.on("session_start", async (_event, ctx) => {
13
12
  runtime.restore(ctx);
14
- if (!runtime.activeState) {
15
- runtime.activeState = restoreLegacyCloudWorkflow(ctx);
16
- if (runtime.activeState) appendWorkflowState(pi, runtime.activeState);
17
- }
18
13
  runtime.clearSecrets();
19
14
  if (!runtime.activeState) return;
20
15
  const details = cloudDetails(runtime.activeState)!;
@@ -29,20 +24,22 @@ export function registerCloudEvents(pi: ExtensionAPI, runtime: CloudExtensionRun
29
24
  ? "Three distinct approaches already failed. Only summarize causes, progress, and changes; do not continue execution."
30
25
  : `Distinct failed approaches: ${details.failedApproaches.length}/${CLOUD_RUNTIME_DEFAULTS.workflow.maxFailedApproaches}. After the third distinct approach fails, stop and summarize.`;
31
26
  return {
32
- systemPrompt: `${event.systemPrompt}\n\nHWCODE CLOUD ACTIVE\nProvider: ${details.vendor}\nObjective: ${details.request}\nArtifact workspace: ${cloudArtifactDirectory(activeState)}\nCredentials must never be requested in chat, printed, placed in tool arguments, or read from the vault. Write all generated Cloud artifacts only under the artifact workspace: discovery/ for CLI skeletons and snapshots, terraform/ for IaC, charts/ for Helm sources/packages, reports/ for plans and summaries. Project source remains under ${activeState.root}; when a provider CLI needs a project source file, pass its absolute project-root path. Use hwcode_cloud_exec for provider CLI operations. ${details.runner ? "Use hwcode_runner_prepare and hwcode_terraform_* for Terraform source synchronization, validation, planning, approval, and apply on the selected Runner." : details.runnerPreference === "automatic" ? "The user requested an automatic temporary Runner. Provision it through approved provider CLI changes with workload identity and cloud-init, then register the discovered endpoint with hwcode_runner_connect." : "No Terraform Runner is selected. Do not provision one unless the user explicitly changes this choice."} Resource creates and changes require approval unless the user opted out; deletion always requires approval. After success, offer /hwcode-cloud-save-template to preserve the validated path locally. ${failureRule}`
27
+ systemPrompt: `${event.systemPrompt}\n\nHWCODE CLOUD ACTIVE\nProvider: ${details.vendor}
28
+ Objective: ${details.request}\nArtifact workspace: ${cloudArtifactDirectory(activeState)}
29
+ Credentials must never be requested in chat, printed, placed in tool arguments, or read from the vault. \
30
+ Write all generated Cloud artifacts only under the artifact workspace: discovery/ for CLI skeletons and \
31
+ snapshots, terraform/ for IaC, charts/ for Helm sources/packages, reports/ for plans and summaries. \
32
+ Project source remains under ${activeState.root}; when a provider CLI needs a project source file, \
33
+ pass its absolute project-root path. Use hwcode_cloud_exec for provider CLI operations. \
34
+ ${details.runner ? "Use hwcode_runner_prepare and hwcode_terraform_* for Terraform source \
35
+ synchronization, validation, planning, approval, and apply on the selected Runner." : details.runnerPreference === "automatic" ? "The user requested an automatic temporary Runner. \
36
+ Provision it through approved provider CLI changes with workload identity and cloud-init, \
37
+ then register the discovered endpoint with hwcode_runner_connect." : "No Terraform Runner is selected. \
38
+ Do not provision one unless the user explicitly changes this choice."} Resource creates and changes require approval unless the user opted out; deletion always requires approval. ${failureRule}`
33
39
  + `\nConcurrency rule: Batch all independent read-only discovery calls concurrently in a single response turn. For resource creation, group independent operations into parallel tool calls while keeping dependent operations strictly sequential.`,
34
40
  };
35
41
  });
36
42
 
37
- pi.on("tool_call", async (event, ctx) => {
38
- return undefined; // TODO: block direct cloud CLI calls during active workflow
39
- // const activeState = runtime.restore(ctx);
40
- // if (!activeState || event.toolName !== "bash") return undefined;
41
- // const input = event.input as Record<string, unknown>;
42
- // if (typeof input.command !== "string" || !containsCloudCommand(input.command)) return undefined;
43
- // return { block: true, reason: "Direct cloud and infrastructure CLI use is blocked during HWCode Cloud. Use hwcode_cloud_exec so credentials remain isolated and approvals are enforced." };
44
- });
45
-
46
43
  pi.on("session_shutdown", async () => {
47
44
  runtime.activeState = undefined;
48
45
  runtime.clearSecrets();