@hadooppei/hwcode 0.2.4 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pi/extensions/hwcode.ts +35 -2
- package/.pi/extensions/model-providers.ts +1 -2
- package/.pi/extensions/workflows/cloud/activation.ts +235 -0
- package/.pi/extensions/workflows/cloud/commands.ts +96 -0
- package/.pi/extensions/workflows/cloud/events.ts +58 -0
- package/.pi/extensions/workflows/cloud/index.ts +17 -0
- package/.pi/extensions/workflows/cloud/provider-tools.ts +127 -0
- package/.pi/extensions/workflows/cloud/runner-tools.ts +129 -0
- package/.pi/extensions/workflows/cloud/runtime.ts +97 -0
- package/.pi/extensions/workflows/cloud/shared.ts +213 -0
- package/.pi/extensions/workflows/cloud/terraform-tools.ts +126 -0
- package/.pi/extensions/workflows/vibe-sdd.ts +300 -0
- package/.pi/extensions/workflows.ts +6 -253
- package/.pi/lib/runtime/config.ts +3 -7
- package/.pi/lib/runtime/defaults.ts +20 -0
- package/.pi/lib/runtime/paths.ts +68 -0
- package/.pi/lib/runtime/session-state.ts +0 -34
- package/.pi/lib/{cloud → workflows/cloud}/adapters.ts +7 -6
- package/.pi/lib/workflows/cloud/bundles.ts +358 -0
- package/.pi/lib/workflows/cloud/execution.ts +28 -0
- package/.pi/lib/{cloud → workflows/cloud}/process.ts +5 -3
- package/.pi/lib/workflows/cloud/remote/bootstrap.ts +23 -0
- package/.pi/lib/workflows/cloud/remote/connect.ts +83 -0
- package/.pi/lib/workflows/cloud/remote/host-key.ts +35 -0
- package/.pi/lib/workflows/cloud/remote/profiles.ts +100 -0
- package/.pi/lib/workflows/cloud/remote/ssh-transport.ts +104 -0
- package/.pi/lib/workflows/cloud/remote/workspace.ts +109 -0
- package/.pi/lib/{cloud → workflows/cloud}/template-save.ts +3 -2
- package/.pi/lib/{cloud → workflows/cloud}/templates.ts +18 -10
- package/.pi/lib/workflows/cloud/terraform/plan.ts +109 -0
- package/.pi/lib/workflows/cloud/terraform/policy.ts +36 -0
- package/.pi/lib/workflows/cloud/terraform/runner.ts +64 -0
- package/.pi/lib/{cloud-vault.ts → workflows/cloud/vault.ts} +26 -24
- package/.pi/lib/workflows/cloud/workspace.ts +37 -0
- package/.pi/lib/workflows/sdd.ts +11 -0
- package/.pi/lib/workflows/state.ts +114 -2
- package/.pi/lib/working-directory.ts +0 -58
- package/.pi/skills/hwcode-cloud/SKILL.md +32 -1
- package/.pi/skills/hwcode-sdd/SKILL.md +2 -0
- package/README.md +41 -8
- package/bin/hwcode.js +2 -6
- package/package.json +8 -3
- package/.pi/extensions/cloud.ts +0 -629
- /package/.pi/lib/{cloud-providers.ts → workflows/cloud/providers.ts} +0 -0
package/.pi/extensions/cloud.ts
DELETED
|
@@ -1,629 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { basename, isAbsolute, resolve } from "node:path";
|
|
4
|
-
|
|
5
|
-
import { Type } from "@earendil-works/pi-ai";
|
|
6
|
-
import { defineTool, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, type Theme } from "@earendil-works/pi-coding-agent";
|
|
7
|
-
import { CURSOR_MARKER, Input, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
8
|
-
|
|
9
|
-
import { checkProviderCli, formatValidationFailure, prepareCloudExecution, validateCloudCredentials, type TemporaryCredentialStore } from "../lib/cloud/adapters.ts";
|
|
10
|
-
import { runProcess, truncateOutput, type ProcessResult } from "../lib/cloud/process.ts";
|
|
11
|
-
import {
|
|
12
|
-
cloudPromptTemplateSource, expandCloudPromptTemplate, listCloudPromptTemplates, saveCloudPromptTemplate, updateCloudPromptTemplate,
|
|
13
|
-
type CloudPromptTemplate,
|
|
14
|
-
} from "../lib/cloud/templates.ts";
|
|
15
|
-
import { saveCloudWorkflowTemplate } from "../lib/cloud/template-save.ts";
|
|
16
|
-
import {
|
|
17
|
-
CLOUD_EXECUTABLES, CLOUD_PROVIDERS, classifyCloudOperation, containsCloudCommand,
|
|
18
|
-
getCloudProvider, inaccessibleCloudCliMessage, missingCloudCliMessage, redactCredentialValues,
|
|
19
|
-
type CloudCredentials, type CloudOperation, type CloudVendorId,
|
|
20
|
-
} from "../lib/cloud-providers.ts";
|
|
21
|
-
import {
|
|
22
|
-
cloudCredentialProfileLabel, cloudVaultExists, createEmptyVault, defaultCloudVaultPath,
|
|
23
|
-
listCloudCredentialProfiles, readCloudVault, saveCloudCredentialProfile, writeCloudVault,
|
|
24
|
-
type CloudCredentialProfile,
|
|
25
|
-
} from "../lib/cloud-vault.ts";
|
|
26
|
-
import { canonicalizeWorkspaceRoot } from "../lib/workflow-guard.ts";
|
|
27
|
-
import { modelConfigurationIssue } from "../lib/models/readiness.ts";
|
|
28
|
-
import {
|
|
29
|
-
WORKFLOW_EXTERNAL_AUDIT_TYPE, WORKFLOW_STATE_TYPE, activeWorkflow, createCloudWorkflowState, updateWorkflowState,
|
|
30
|
-
type CloudWorkflowDetails, type WorkflowState,
|
|
31
|
-
} from "../lib/workflows/state.ts";
|
|
32
|
-
import { evaluateCommandArgumentsAccess } from "../lib/workspace/access-policy.ts";
|
|
33
|
-
import { getWorkingDirectory } from "../lib/working-directory.ts";
|
|
34
|
-
|
|
35
|
-
const LEGACY_CLOUD_STATE_TYPE = "hwcode-cloud-state";
|
|
36
|
-
const CLOUD_AUDIT_TYPE = "hwcode-cloud-audit";
|
|
37
|
-
const MAX_FAILED_APPROACHES = 3;
|
|
38
|
-
const MAX_SUCCESSFUL_STEPS = 100;
|
|
39
|
-
const ORCHESTRATION_COMMANDS = new Set(["terraform", "tofu", "pulumi", "kubectl", "helm"]);
|
|
40
|
-
const SENSITIVE_ARGUMENT = /^--?(?:access[-_]?key|secret(?:[-_]?(?:access|key))?|client[-_]?secret|password|credential|token)(?:=|$)/iu;
|
|
41
|
-
|
|
42
|
-
interface LegacyCloudWorkflowState {
|
|
43
|
-
version: 1;
|
|
44
|
-
active: true;
|
|
45
|
-
root: string;
|
|
46
|
-
vendor: CloudVendorId;
|
|
47
|
-
deployCurrentProject: boolean;
|
|
48
|
-
request: string;
|
|
49
|
-
allowNonDeleteChanges: boolean;
|
|
50
|
-
failedApproaches: CloudWorkflowDetails["failedApproaches"];
|
|
51
|
-
terminalFailure: boolean;
|
|
52
|
-
activatedAt: string;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
class MaskedInput implements Component {
|
|
56
|
-
private readonly input = new Input();
|
|
57
|
-
private _focused = true;
|
|
58
|
-
private readonly tui: TUI;
|
|
59
|
-
private readonly theme: Theme;
|
|
60
|
-
private readonly title: string;
|
|
61
|
-
|
|
62
|
-
constructor(tui: TUI, theme: Theme, title: string, done: (value: string | undefined) => void) {
|
|
63
|
-
this.tui = tui;
|
|
64
|
-
this.theme = theme;
|
|
65
|
-
this.title = title;
|
|
66
|
-
this.input.onSubmit = (value) => done(value);
|
|
67
|
-
this.input.onEscape = () => done(undefined);
|
|
68
|
-
this.input.focused = true;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
get focused(): boolean { return this._focused; }
|
|
72
|
-
set focused(value: boolean) { this._focused = value; this.input.focused = value; }
|
|
73
|
-
handleInput(data: string): void { this.input.handleInput(data); this.tui.requestRender(); }
|
|
74
|
-
invalidate(): void { this.input.invalidate(); }
|
|
75
|
-
render(width: number): string[] {
|
|
76
|
-
const masked = "•".repeat(Math.min(Array.from(this.input.getValue()).length, Math.max(1, width - 1)));
|
|
77
|
-
return [this.theme.fg("accent", this.title), `${masked}${CURSOR_MARKER}`, this.theme.fg("dim", "Enter 确认 · Esc 取消 · 输入内容不会显示")];
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function notify(ctx: ExtensionContext, message: string, level: "info" | "warning" | "error" = "info"): void {
|
|
82
|
-
if (ctx.hasUI) ctx.ui.notify(message, level);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function cloudDetails(state: WorkflowState): CloudWorkflowDetails | undefined {
|
|
86
|
-
return state.mode === "cloud" ? state.details : undefined;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function formatTemplateTime(value: string): string {
|
|
90
|
-
const date = new Date(value);
|
|
91
|
-
if (Number.isNaN(date.valueOf())) return value;
|
|
92
|
-
const pad = (part: number) => String(part).padStart(2, "0");
|
|
93
|
-
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function restoreCloudWorkflow(ctx: ExtensionContext): WorkflowState | undefined {
|
|
97
|
-
const state = activeWorkflow(ctx.sessionManager.getEntries());
|
|
98
|
-
return state?.mode === "cloud" && state.details ? state : undefined;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function decodeLegacyCloudState(value: unknown): LegacyCloudWorkflowState | undefined {
|
|
102
|
-
if (!value || typeof value !== "object") return undefined;
|
|
103
|
-
const data = value as Record<string, unknown>;
|
|
104
|
-
if (data.version !== 1 || data.active !== true || typeof data.root !== "string"
|
|
105
|
-
|| typeof data.vendor !== "string" || !CLOUD_PROVIDERS.some((provider) => provider.id === data.vendor)
|
|
106
|
-
|| typeof data.deployCurrentProject !== "boolean" || typeof data.request !== "string"
|
|
107
|
-
|| typeof data.allowNonDeleteChanges !== "boolean" || !Array.isArray(data.failedApproaches)
|
|
108
|
-
|| typeof data.terminalFailure !== "boolean" || typeof data.activatedAt !== "string") return undefined;
|
|
109
|
-
return data as unknown as LegacyCloudWorkflowState;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function restoreLegacyCloudWorkflow(ctx: ExtensionContext): WorkflowState | undefined {
|
|
113
|
-
for (const entry of [...ctx.sessionManager.getEntries()].reverse()) {
|
|
114
|
-
if (entry.type !== "custom" || entry.customType !== LEGACY_CLOUD_STATE_TYPE) continue;
|
|
115
|
-
const legacy = decodeLegacyCloudState(entry.data);
|
|
116
|
-
if (!legacy) return undefined;
|
|
117
|
-
return {
|
|
118
|
-
version: 2, status: "active", mode: "cloud", root: legacy.root,
|
|
119
|
-
phase: legacy.terminalFailure ? "failed" : "connected",
|
|
120
|
-
activatedAt: legacy.activatedAt, updatedAt: legacy.activatedAt,
|
|
121
|
-
details: {
|
|
122
|
-
vendor: legacy.vendor, deployCurrentProject: legacy.deployCurrentProject,
|
|
123
|
-
request: legacy.request, allowNonDeleteChanges: legacy.allowNonDeleteChanges,
|
|
124
|
-
failedApproaches: legacy.failedApproaches, successfulSteps: [], terminalFailure: legacy.terminalFailure,
|
|
125
|
-
},
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
return undefined;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function appendWorkflowState(pi: ExtensionAPI, state: WorkflowState): void {
|
|
132
|
-
pi.appendEntry<WorkflowState>(WORKFLOW_STATE_TYPE, state);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
async function secretInput(ctx: ExtensionContext, title: string): Promise<string | undefined> {
|
|
136
|
-
if (ctx.mode !== "tui") return undefined;
|
|
137
|
-
return ctx.ui.custom<string | undefined>((tui, theme, _keybindings, done) => new MaskedInput(tui, theme, title, done));
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async function unlockVault(ctx: ExtensionCommandContext): Promise<{ password: string; payload: ReturnType<typeof createEmptyVault> } | undefined> {
|
|
141
|
-
const vaultPath = defaultCloudVaultPath();
|
|
142
|
-
if (!cloudVaultExists(vaultPath)) {
|
|
143
|
-
while (true) {
|
|
144
|
-
const password = await secretInput(ctx, "创建云凭据主密码(至少 8 个字符)");
|
|
145
|
-
if (password === undefined) return undefined;
|
|
146
|
-
if (password.length < 8) { notify(ctx, "主密码至少需要 8 个字符。", "warning"); continue; }
|
|
147
|
-
const confirmation = await secretInput(ctx, "再次输入主密码");
|
|
148
|
-
if (confirmation === undefined) return undefined;
|
|
149
|
-
if (confirmation !== password) { notify(ctx, "两次输入的主密码不一致。", "warning"); continue; }
|
|
150
|
-
return { password, payload: createEmptyVault() };
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
while (true) {
|
|
155
|
-
const password = await secretInput(ctx, "输入云凭据主密码以解锁");
|
|
156
|
-
if (password === undefined) return undefined;
|
|
157
|
-
try { return { password, payload: readCloudVault(password, vaultPath) }; }
|
|
158
|
-
catch (error) {
|
|
159
|
-
notify(ctx, error instanceof Error ? error.message : String(error), "error");
|
|
160
|
-
if (!(await ctx.ui.confirm("重新输入主密码?", "凭据未解锁,HWCode Cloud 尚未启动。"))) return undefined;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
async function collectCredentials(vendor: CloudVendorId, root: string, ctx: ExtensionCommandContext): Promise<CloudCredentials | undefined> {
|
|
166
|
-
const credentials: CloudCredentials = {};
|
|
167
|
-
for (const field of getCloudProvider(vendor).credentialFields) {
|
|
168
|
-
let value = field.secret ? await secretInput(ctx, field.label) : await ctx.ui.input(field.label, field.placeholder);
|
|
169
|
-
if (value === undefined) return undefined;
|
|
170
|
-
value = value.trim();
|
|
171
|
-
if (!value && !field.optional) { notify(ctx, `${field.label} 不能为空。`, "warning"); return collectCredentials(vendor, root, ctx); }
|
|
172
|
-
if (!value) continue;
|
|
173
|
-
if (field.fileContents) {
|
|
174
|
-
const path = isAbsolute(value) ? value : resolve(root, value);
|
|
175
|
-
try { const contents = readFileSync(path, "utf8"); JSON.parse(contents); credentials[field.key] = contents; }
|
|
176
|
-
catch (error) {
|
|
177
|
-
notify(ctx, `无法读取有效的 JSON 凭据文件 ${path}: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
178
|
-
return collectCredentials(vendor, root, ctx);
|
|
179
|
-
}
|
|
180
|
-
} else credentials[field.key] = value;
|
|
181
|
-
}
|
|
182
|
-
if (vendor === "gcp") {
|
|
183
|
-
const account = JSON.parse(credentials.serviceAccountJson) as Record<string, unknown>;
|
|
184
|
-
credentials.projectId ||= typeof account.project_id === "string" ? account.project_id : "";
|
|
185
|
-
if (!credentials.projectId) { notify(ctx, "Service Account JSON 中没有 project_id,请重新输入。", "error"); return collectCredentials(vendor, root, ctx); }
|
|
186
|
-
}
|
|
187
|
-
return credentials;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function activationPrompt(state: WorkflowState): string {
|
|
191
|
-
const details = cloudDetails(state)!;
|
|
192
|
-
const guidance = details.templateGuidance
|
|
193
|
-
? `\n\nReusable template guidance (${details.sourceTemplate?.name ?? "saved template"}):\n${details.templateGuidance}`
|
|
194
|
-
: "";
|
|
195
|
-
return `/skill:hwcode-cloud HWCode Cloud workflow activated.\n\nLocked project root: ${state.root}\nCloud provider: ${getCloudProvider(details.vendor).label}\nDeploy current project: ${details.deployCurrentProject ? "yes" : "no"}\nUser objective:\n${details.request}${guidance}\n\nCredentials were validated and remain outside model context. Use hwcode_cloud_exec for every cloud or infrastructure command.`;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
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 details = cloudDetails(state)!;
|
|
202
|
-
const command = `${params.command} ${params.args.join(" ")}`;
|
|
203
|
-
if (operation === "delete") return await ctx.ui.confirm("确认删除云资源?", `目标:${params.intent}\n命令:${command}\n\n删除操作始终逐次确认。`) ? "allow" : "deny";
|
|
204
|
-
if (details.allowNonDeleteChanges) return "allow";
|
|
205
|
-
const choice = await ctx.ui.select("允许修改云账号内资源?", ["仅允许本次", "允许本会话后续非删除变更,不再询问", "拒绝"]);
|
|
206
|
-
if (choice === "仅允许本次") return "allow";
|
|
207
|
-
if (choice === "允许本会话后续非删除变更,不再询问") return "allow-session";
|
|
208
|
-
return "deny";
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function normalizeApproach(value: string): string { return value.trim().toLowerCase().replace(/\s+/gu, " ").slice(0, 160); }
|
|
212
|
-
function safeStepText(value: string, credentials: CloudCredentials): string {
|
|
213
|
-
return redactCredentialValues(value.replace(/[\r\n]+/gu, " ").slice(0, 2_000), credentials);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
export default function cloudExtension(pi: ExtensionAPI) {
|
|
217
|
-
let activeState: WorkflowState | undefined;
|
|
218
|
-
let activeCredentials: CloudCredentials | undefined;
|
|
219
|
-
const credentialDirectories = new Set<string>();
|
|
220
|
-
const temporaryStore: TemporaryCredentialStore = {
|
|
221
|
-
createDirectory(prefix) {
|
|
222
|
-
const directory = mkdtempSync(`${tmpdir()}/${prefix}`);
|
|
223
|
-
credentialDirectories.add(directory);
|
|
224
|
-
return directory;
|
|
225
|
-
},
|
|
226
|
-
cleanupDirectory(directory) {
|
|
227
|
-
if (existsSync(directory)) rmSync(directory, { recursive: true, force: true });
|
|
228
|
-
credentialDirectories.delete(directory);
|
|
229
|
-
},
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
function cleanupCredentialDirectories(): void {
|
|
233
|
-
for (const directory of [...credentialDirectories]) temporaryStore.cleanupDirectory(directory);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function replaceDetails(state: WorkflowState, details: CloudWorkflowDetails, phase = state.phase): WorkflowState {
|
|
237
|
-
const updated = updateWorkflowState(state, { details, phase });
|
|
238
|
-
activeState = updated;
|
|
239
|
-
appendWorkflowState(pi, updated);
|
|
240
|
-
return updated;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
async function ensureConversationModel(ctx: ExtensionCommandContext): Promise<boolean> {
|
|
244
|
-
let environment: Record<string, string | undefined> = {};
|
|
245
|
-
if (ctx.model) {
|
|
246
|
-
try {
|
|
247
|
-
environment = (await ctx.modelRegistry.getProviderAuth(ctx.model.provider))?.env ?? {};
|
|
248
|
-
} catch {
|
|
249
|
-
// The readiness check below reports the actionable model configuration error.
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
const issue = modelConfigurationIssue(ctx.model, environment);
|
|
253
|
-
if (!issue) return true;
|
|
254
|
-
notify(
|
|
255
|
-
ctx,
|
|
256
|
-
`${issue}。HWCode 不会替你选择或覆盖默认模型;请先使用 Pi 原生 /model 切换模型,或补全该 provider 的 endpoint 配置。`,
|
|
257
|
-
"error",
|
|
258
|
-
);
|
|
259
|
-
return false;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
async function chooseTemplate(ctx: ExtensionCommandContext): Promise<CloudPromptTemplate | undefined> {
|
|
263
|
-
const templates = listCloudPromptTemplates();
|
|
264
|
-
if (templates.length === 0) {
|
|
265
|
-
notify(ctx, "还没有本地 Cloud Prompt Template。成功执行任务后使用 /hwcode-cloud-save-template 保存。", "warning");
|
|
266
|
-
return undefined;
|
|
267
|
-
}
|
|
268
|
-
const labels = templates.map((template) => `${template.name} · ${formatTemplateTime(template.updatedAt)}`);
|
|
269
|
-
const selected = await ctx.ui.select(
|
|
270
|
-
"选择已验证的 Cloud Prompt Template",
|
|
271
|
-
labels,
|
|
272
|
-
);
|
|
273
|
-
const selectedIndex = selected ? labels.indexOf(selected) : -1;
|
|
274
|
-
return selectedIndex >= 0 ? templates[selectedIndex] : undefined;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
async function activate(args: string, ctx: ExtensionCommandContext, initialTemplate?: CloudPromptTemplate): Promise<void> {
|
|
278
|
-
if (ctx.mode !== "tui") { notify(ctx, "HWCode Cloud 要求在交互式 TUI 中启动,以安全遮罩凭据输入。", "error"); return; }
|
|
279
|
-
if (!ctx.isIdle()) { notify(ctx, "请等待当前响应完成后再启动 HWCode Cloud。", "warning"); return; }
|
|
280
|
-
if (!(await ensureConversationModel(ctx))) return;
|
|
281
|
-
|
|
282
|
-
const root = canonicalizeWorkspaceRoot(getWorkingDirectory(ctx.sessionManager));
|
|
283
|
-
const currentWorkflow = activeWorkflow(ctx.sessionManager.getEntries());
|
|
284
|
-
if (currentWorkflow && currentWorkflow.mode !== "cloud") {
|
|
285
|
-
notify(ctx, `当前会话已有 ${currentWorkflow.mode} workflow。请新建 session 后再启动 HWCode Cloud。`, "warning");
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
const storedState = restoreCloudWorkflow(ctx) ?? restoreLegacyCloudWorkflow(ctx);
|
|
289
|
-
let resumedState: WorkflowState | undefined;
|
|
290
|
-
if (storedState && storedState.root === root && !cloudDetails(storedState)?.terminalFailure) {
|
|
291
|
-
const storedDetails = cloudDetails(storedState)!;
|
|
292
|
-
const choice = await ctx.ui.select("发现当前会话中未完成的 HWCode Cloud workflow", [
|
|
293
|
-
`恢复 ${getCloudProvider(storedDetails.vendor).label}:${storedDetails.request.slice(0, 60)}`,
|
|
294
|
-
"开始新的 Cloud workflow",
|
|
295
|
-
"取消",
|
|
296
|
-
]);
|
|
297
|
-
if (!choice || choice === "取消") return;
|
|
298
|
-
if (choice.startsWith("恢复 ")) resumedState = storedState;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
let template = initialTemplate;
|
|
302
|
-
if (!resumedState && !template && !args.trim() && listCloudPromptTemplates().length > 0) {
|
|
303
|
-
const source = await ctx.ui.select("Cloud 任务来源", ["新建任务", "使用已有 Prompt Template", "取消"]);
|
|
304
|
-
if (!source || source === "取消") return;
|
|
305
|
-
if (source === "使用已有 Prompt Template") {
|
|
306
|
-
template = await chooseTemplate(ctx);
|
|
307
|
-
if (!template) return;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
let vendor: CloudVendorId;
|
|
312
|
-
let deployCurrentProject: boolean;
|
|
313
|
-
let request: string;
|
|
314
|
-
let templateGuidance: string | undefined;
|
|
315
|
-
if (resumedState) {
|
|
316
|
-
const details = cloudDetails(resumedState)!;
|
|
317
|
-
vendor = details.vendor;
|
|
318
|
-
deployCurrentProject = details.deployCurrentProject;
|
|
319
|
-
request = details.request;
|
|
320
|
-
} else if (template) {
|
|
321
|
-
vendor = template.vendor;
|
|
322
|
-
deployCurrentProject = template.deployCurrentProject;
|
|
323
|
-
request = template.objective || template.name;
|
|
324
|
-
templateGuidance = expandCloudPromptTemplate(template, args);
|
|
325
|
-
} else {
|
|
326
|
-
const label = await ctx.ui.select("选择需要对接的云计算厂商", CLOUD_PROVIDERS.map((provider) => provider.label));
|
|
327
|
-
if (!label) return;
|
|
328
|
-
vendor = CLOUD_PROVIDERS.find((provider) => provider.label === label)!.id;
|
|
329
|
-
const deployment = await ctx.ui.select("是否部署当前项目?", ["是,部署当前项目", "否,执行其他云任务"]);
|
|
330
|
-
if (!deployment) return;
|
|
331
|
-
deployCurrentProject = deployment.startsWith("是");
|
|
332
|
-
const entered = (await ctx.ui.editor("描述需要执行的具体需求", args.trim()))?.trim();
|
|
333
|
-
if (!entered) { notify(ctx, "必须提供明确的云任务需求,HWCode Cloud 未启动。", "warning"); return; }
|
|
334
|
-
request = entered;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const vendorLabel = getCloudProvider(vendor).label;
|
|
338
|
-
const cliCheck = await checkProviderCli(vendor, root);
|
|
339
|
-
if (cliCheck.spawnErrorCode === "ENOENT") { notify(ctx, missingCloudCliMessage(vendor), "error"); return; }
|
|
340
|
-
if (cliCheck.spawnErrorCode === "EACCES") { notify(ctx, inaccessibleCloudCliMessage(vendor), "error"); return; }
|
|
341
|
-
|
|
342
|
-
const vaultPath = defaultCloudVaultPath();
|
|
343
|
-
if (!(await ctx.ui.confirm("安全保存云账户信息?", [
|
|
344
|
-
`当前项目根目录将锁定为:\n${root}`,
|
|
345
|
-
`凭据将使用主密码派生密钥,以 AES-256-GCM 加密保存到:\n${vaultPath}`,
|
|
346
|
-
"文件权限限制为当前用户读写,不会发送给模型或上传至 HWCode 服务。",
|
|
347
|
-
"连接校验和后续命令只会把凭据提交给所选云厂商的官方认证端点或 CLI。",
|
|
348
|
-
"如果忘记主密码,加密凭据无法恢复。",
|
|
349
|
-
].join("\n\n")))) return;
|
|
350
|
-
|
|
351
|
-
const unlocked = await unlockVault(ctx);
|
|
352
|
-
if (!unlocked) return;
|
|
353
|
-
const profiles = listCloudCredentialProfiles(unlocked.payload, vendor);
|
|
354
|
-
let selectedProfile: CloudCredentialProfile | undefined;
|
|
355
|
-
let credentials: CloudCredentials | undefined;
|
|
356
|
-
if (profiles.length > 0) {
|
|
357
|
-
const labels = profiles.map((profile, index) => cloudCredentialProfileLabel(profile, index, profiles.length));
|
|
358
|
-
const choice = await ctx.ui.select(`选择 ${vendorLabel} 凭据`, [...labels, "新建凭据", "取消"]);
|
|
359
|
-
if (!choice || choice === "取消") return;
|
|
360
|
-
const selectedIndex = labels.indexOf(choice);
|
|
361
|
-
if (selectedIndex >= 0) {
|
|
362
|
-
selectedProfile = profiles[selectedIndex];
|
|
363
|
-
credentials = { ...selectedProfile.credentials };
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
while (true) {
|
|
368
|
-
credentials ??= await collectCredentials(vendor, root, ctx);
|
|
369
|
-
if (!credentials) return;
|
|
370
|
-
notify(ctx, `正在校验 ${vendorLabel} 连接…`);
|
|
371
|
-
const validation = await validateCloudCredentials(vendor, { root, credentials, temporaryStore });
|
|
372
|
-
if (validation.code === 0) break;
|
|
373
|
-
if (validation.spawnErrorCode === "ENOENT") { notify(ctx, missingCloudCliMessage(vendor), "error"); return; }
|
|
374
|
-
if (validation.spawnErrorCode === "EACCES") { notify(ctx, inaccessibleCloudCliMessage(vendor), "error"); return; }
|
|
375
|
-
notify(ctx, `连接校验失败:${formatValidationFailure(validation, credentials)}`, "error");
|
|
376
|
-
if (!(await ctx.ui.confirm("重新输入账户信息?", "连接尚未建立。请检查凭据、账号状态、网络及对应云 CLI。"))) return;
|
|
377
|
-
credentials = undefined;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
writeCloudVault(
|
|
381
|
-
saveCloudCredentialProfile(unlocked.payload, vendor, credentials, selectedProfile?.id),
|
|
382
|
-
unlocked.password,
|
|
383
|
-
vaultPath,
|
|
384
|
-
);
|
|
385
|
-
cleanupCredentialDirectories();
|
|
386
|
-
activeCredentials = credentials;
|
|
387
|
-
activeState = resumedState ?? createCloudWorkflowState(
|
|
388
|
-
root,
|
|
389
|
-
vendor,
|
|
390
|
-
deployCurrentProject,
|
|
391
|
-
request,
|
|
392
|
-
template && templateGuidance ? {
|
|
393
|
-
source: cloudPromptTemplateSource(template),
|
|
394
|
-
guidance: templateGuidance,
|
|
395
|
-
} : undefined,
|
|
396
|
-
);
|
|
397
|
-
appendWorkflowState(pi, activeState);
|
|
398
|
-
notify(ctx, `${vendorLabel} 连接成功。凭据已加密保存,项目根目录锁定为 ${root}。`);
|
|
399
|
-
pi.sendUserMessage(activationPrompt(activeState), { expandPromptTemplates: true });
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
pi.registerCommand("hwcode-cloud", {
|
|
403
|
-
description: "Start or resume the guarded HWCode cloud workflow",
|
|
404
|
-
handler: (args, ctx) => activate(args, ctx),
|
|
405
|
-
});
|
|
406
|
-
pi.registerCommand("hwcode-cloud-template", {
|
|
407
|
-
description: "Start HWCode Cloud from a locally saved successful template",
|
|
408
|
-
handler: async (args, ctx) => {
|
|
409
|
-
const template = await chooseTemplate(ctx);
|
|
410
|
-
if (template) await activate(args, ctx, template);
|
|
411
|
-
},
|
|
412
|
-
});
|
|
413
|
-
pi.registerCommand("hwcode-cloud-save-template", {
|
|
414
|
-
description: "Save the successful Cloud execution path as a local Cloud Prompt Template",
|
|
415
|
-
handler: async (args, ctx) => {
|
|
416
|
-
activeState = restoreCloudWorkflow(ctx) ?? activeState;
|
|
417
|
-
const details = activeState && cloudDetails(activeState);
|
|
418
|
-
if (!activeState || !details || details.successfulSteps.length === 0) {
|
|
419
|
-
notify(ctx, "当前 Cloud workflow 还没有成功执行步骤,无法生成可复用模板。", "warning");
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
const result = await saveCloudWorkflowTemplate({
|
|
424
|
-
state: activeState,
|
|
425
|
-
requestedName: args,
|
|
426
|
-
redact: activeCredentials
|
|
427
|
-
? (value) => redactCredentialValues(value, activeCredentials!)
|
|
428
|
-
: undefined,
|
|
429
|
-
interaction: {
|
|
430
|
-
async chooseSourceAction(source) {
|
|
431
|
-
const updateLabel = `更新原模板「${source.name}」`;
|
|
432
|
-
const choice = await ctx.ui.select(
|
|
433
|
-
`当前会话关联模板「${details.sourceTemplate?.name ?? source.name}」`,
|
|
434
|
-
[updateLabel, "创建新模板", "取消"],
|
|
435
|
-
);
|
|
436
|
-
if (!choice || choice === "取消") return "cancel";
|
|
437
|
-
return choice === updateLabel ? "update" : "create";
|
|
438
|
-
},
|
|
439
|
-
inputName: (defaultName) => ctx.ui.input("模板名称", defaultName),
|
|
440
|
-
editNotes: (initialNotes) => ctx.ui.editor(
|
|
441
|
-
"补充必要前置条件、成功经验及必须避免的高消耗错误路径(可留空;不得包含凭据)",
|
|
442
|
-
initialNotes,
|
|
443
|
-
),
|
|
444
|
-
sourceMissing(sourceName) {
|
|
445
|
-
notify(ctx, `本次使用的原模板「${sourceName}」已不存在,将创建新模板。`, "warning");
|
|
446
|
-
},
|
|
447
|
-
},
|
|
448
|
-
store: {
|
|
449
|
-
list: () => listCloudPromptTemplates(),
|
|
450
|
-
create: (name, state, notes) => saveCloudPromptTemplate(name, state, undefined, notes),
|
|
451
|
-
update: (template, state, notes) => updateCloudPromptTemplate(template, state, notes),
|
|
452
|
-
},
|
|
453
|
-
});
|
|
454
|
-
if (result.status === "cancelled") return;
|
|
455
|
-
activeState = replaceDetails(activeState, result.details);
|
|
456
|
-
const action = result.action === "updated" ? "已更新" : "已创建";
|
|
457
|
-
notify(ctx, `${action}模板「${result.template.name}」(${formatTemplateTime(result.template.updatedAt)})。使用 /hwcode-cloud-template 可立即复用。`);
|
|
458
|
-
},
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
pi.registerTool(defineTool({
|
|
462
|
-
name: "hwcode_cloud_exec",
|
|
463
|
-
label: "HWCode Cloud",
|
|
464
|
-
description: "Run one cloud or infrastructure CLI command with isolated credentials and unified workspace/resource approvals.",
|
|
465
|
-
promptSnippet: "Execute credential-isolated cloud CLI commands with mandatory resource-change approvals.",
|
|
466
|
-
parameters: Type.Object({
|
|
467
|
-
command: Type.String({ description: "Executable name only" }),
|
|
468
|
-
args: Type.Array(Type.String(), { description: "Argument vector; never include credentials or shell syntax" }),
|
|
469
|
-
operation: Type.Union([Type.Literal("read"), Type.Literal("change"), Type.Literal("delete")]),
|
|
470
|
-
intent: Type.String({ description: "Concise description of what this command inspects or changes" }),
|
|
471
|
-
approach: Type.String({ description: "Stable name for this technical approach" }),
|
|
472
|
-
}),
|
|
473
|
-
executionMode: "sequential",
|
|
474
|
-
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
475
|
-
activeState = restoreCloudWorkflow(ctx) ?? activeState;
|
|
476
|
-
let details = activeState && cloudDetails(activeState);
|
|
477
|
-
if (!activeState || !details) {
|
|
478
|
-
return { content: [{ type: "text", text: "HWCode Cloud is not active. Run /hwcode-cloud first." }], isError: true, details: {} };
|
|
479
|
-
}
|
|
480
|
-
if (details.terminalFailure) {
|
|
481
|
-
return { content: [{ type: "text", text: "HWCode Cloud stopped after three distinct failed approaches. Summarize the reason, progress, and changes; do not execute more cloud operations." }], isError: true, details: { terminalFailure: true } };
|
|
482
|
-
}
|
|
483
|
-
if (!activeCredentials) {
|
|
484
|
-
return { content: [{ type: "text", text: "Cloud credentials are locked after restoration. Run /hwcode-cloud to unlock and validate them." }], isError: true, details: {} };
|
|
485
|
-
}
|
|
486
|
-
if (basename(params.command) !== params.command || !CLOUD_EXECUTABLES.has(params.command)) {
|
|
487
|
-
return { content: [{ type: "text", text: `Unsupported executable: ${params.command}` }], isError: true, details: {} };
|
|
488
|
-
}
|
|
489
|
-
const providerCli = getCloudProvider(details.vendor).cli;
|
|
490
|
-
if (params.command !== providerCli && !ORCHESTRATION_COMMANDS.has(params.command)) {
|
|
491
|
-
return { content: [{ type: "text", text: `${params.command} does not match the active ${details.vendor} provider.` }], isError: true, details: {} };
|
|
492
|
-
}
|
|
493
|
-
if (params.args.some((argument) => SENSITIVE_ARGUMENT.test(argument))) {
|
|
494
|
-
return { content: [{ type: "text", text: "Credential-like command arguments are forbidden. HWCode injects credentials outside model context." }], isError: true, details: {} };
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
const operation = classifyCloudOperation(params.command, params.args, params.operation as CloudOperation);
|
|
498
|
-
const external = evaluateCommandArgumentsAccess(params.command, params.args, activeState.root);
|
|
499
|
-
if (external.length > 0) {
|
|
500
|
-
const summary = external.map((entry) => `• ${entry.raw} → ${entry.resolved}`).join("\n");
|
|
501
|
-
if (!ctx.hasUI || !(await ctx.ui.confirm(
|
|
502
|
-
"允许本次云工具访问项目目录之外的路径?",
|
|
503
|
-
`项目根目录:${activeState.root}\n\n${summary}\n\n该授权仅适用于本次命令。`,
|
|
504
|
-
))) {
|
|
505
|
-
return { content: [{ type: "text", text: `User denied external path access:\n${summary}` }], isError: true, details: { denied: true } };
|
|
506
|
-
}
|
|
507
|
-
pi.appendEntry(WORKFLOW_EXTERNAL_AUDIT_TYPE, {
|
|
508
|
-
mode: "cloud", root: activeState.root, toolName: "hwcode_cloud_exec", external,
|
|
509
|
-
approvedAt: new Date().toISOString(),
|
|
510
|
-
});
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
const approval = await approveOperation(operation, params, activeState, ctx);
|
|
514
|
-
if (approval === "deny") {
|
|
515
|
-
return { content: [{ type: "text", text: `User denied ${operation} operation: ${params.intent}` }], isError: true, details: { denied: true, operation } };
|
|
516
|
-
}
|
|
517
|
-
if (approval === "allow-session") {
|
|
518
|
-
activeState = replaceDetails(activeState, { ...details, allowNonDeleteChanges: true });
|
|
519
|
-
details = cloudDetails(activeState)!;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
const prepared = await prepareCloudExecution(details.vendor, params.command, params.args, {
|
|
523
|
-
root: activeState.root, credentials: activeCredentials, temporaryStore, signal,
|
|
524
|
-
});
|
|
525
|
-
let result: ProcessResult;
|
|
526
|
-
try {
|
|
527
|
-
result = prepared.error
|
|
528
|
-
? { stdout: "", stderr: prepared.error, code: 1, killed: false }
|
|
529
|
-
: await runProcess(params.command, prepared.args, { cwd: activeState.root, env: prepared.env, signal });
|
|
530
|
-
} finally { prepared.cleanup?.(); }
|
|
531
|
-
|
|
532
|
-
const stdout = truncateOutput(redactCredentialValues(result.stdout, activeCredentials));
|
|
533
|
-
const stderr = truncateOutput(redactCredentialValues(result.stderr, activeCredentials));
|
|
534
|
-
pi.appendEntry(CLOUD_AUDIT_TYPE, {
|
|
535
|
-
vendor: details.vendor, command: params.command, operation, intent: params.intent,
|
|
536
|
-
approach: params.approach, exitCode: result.code, executedAt: new Date().toISOString(),
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
if (result.code !== 0) {
|
|
540
|
-
const normalized = normalizeApproach(params.approach);
|
|
541
|
-
const alreadyFailed = details.failedApproaches.some((failure) => normalizeApproach(failure.approach) === normalized);
|
|
542
|
-
const failedApproaches = !alreadyFailed && normalized
|
|
543
|
-
? [...details.failedApproaches, {
|
|
544
|
-
approach: params.approach.trim(),
|
|
545
|
-
reason: (stderr.trim() || stdout.trim() || `exit code ${result.code}`).slice(0, 1_000),
|
|
546
|
-
failedAt: new Date().toISOString(),
|
|
547
|
-
}]
|
|
548
|
-
: details.failedApproaches;
|
|
549
|
-
const terminalFailure = failedApproaches.length >= MAX_FAILED_APPROACHES;
|
|
550
|
-
replaceDetails(activeState, { ...details, failedApproaches, terminalFailure }, terminalFailure ? "failed" : "executing");
|
|
551
|
-
const suffix = terminalFailure
|
|
552
|
-
? "\n\nTERMINAL FAILURE: Three materially different approaches failed. Explain causes and summarize progress and changes. Do not try another cloud operation."
|
|
553
|
-
: `\n\nDistinct failed approaches: ${failedApproaches.length}/${MAX_FAILED_APPROACHES}.`;
|
|
554
|
-
return {
|
|
555
|
-
content: [{ type: "text", text: `Cloud command failed with exit code ${result.code}.\n${stderr || stdout}${suffix}` }],
|
|
556
|
-
isError: true,
|
|
557
|
-
details: { operation, exitCode: result.code, terminalFailure },
|
|
558
|
-
};
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
const successfulSteps = [...details.successfulSteps, {
|
|
562
|
-
command: safeStepText(params.command, activeCredentials),
|
|
563
|
-
args: params.args.map((argument) => safeStepText(argument, activeCredentials)),
|
|
564
|
-
operation,
|
|
565
|
-
intent: safeStepText(params.intent, activeCredentials),
|
|
566
|
-
approach: safeStepText(params.approach, activeCredentials),
|
|
567
|
-
completedAt: new Date().toISOString(),
|
|
568
|
-
}].slice(-MAX_SUCCESSFUL_STEPS);
|
|
569
|
-
replaceDetails(activeState, { ...details, successfulSteps }, "executing");
|
|
570
|
-
return {
|
|
571
|
-
content: [{ type: "text", text: stdout.trim() || stderr.trim() || "Cloud command completed successfully." }],
|
|
572
|
-
details: { operation, exitCode: 0 },
|
|
573
|
-
};
|
|
574
|
-
},
|
|
575
|
-
}));
|
|
576
|
-
|
|
577
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
578
|
-
activeState = restoreCloudWorkflow(ctx);
|
|
579
|
-
if (!activeState) {
|
|
580
|
-
activeState = restoreLegacyCloudWorkflow(ctx);
|
|
581
|
-
if (activeState) appendWorkflowState(pi, activeState);
|
|
582
|
-
}
|
|
583
|
-
activeCredentials = undefined;
|
|
584
|
-
cleanupCredentialDirectories();
|
|
585
|
-
if (!activeState) return;
|
|
586
|
-
const root = canonicalizeWorkspaceRoot(getWorkingDirectory(ctx.sessionManager));
|
|
587
|
-
if (root !== activeState.root) {
|
|
588
|
-
appendWorkflowState(pi, updateWorkflowState(activeState, {
|
|
589
|
-
status: "cancelled", phase: "root-changed", reason: "working directory changed",
|
|
590
|
-
}));
|
|
591
|
-
activeState = undefined;
|
|
592
|
-
notify(ctx, "Stored HWCode Cloud workflow disabled because the current directory changed.", "warning");
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
|
-
const details = cloudDetails(activeState)!;
|
|
596
|
-
if (!details.terminalFailure) {
|
|
597
|
-
notify(ctx, `HWCode Cloud session restored for ${getCloudProvider(details.vendor).label}. Credentials are locked; run /hwcode-cloud to unlock before continuing.`, "warning");
|
|
598
|
-
}
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
pi.on("before_agent_start", async (event, ctx) => {
|
|
602
|
-
activeState = restoreCloudWorkflow(ctx) ?? activeState;
|
|
603
|
-
const details = activeState && cloudDetails(activeState);
|
|
604
|
-
if (!activeState || !details) return undefined;
|
|
605
|
-
const failureRule = details.terminalFailure
|
|
606
|
-
? "Three distinct approaches already failed. Only summarize causes, progress, and changes; do not continue execution."
|
|
607
|
-
: `Distinct failed approaches: ${details.failedApproaches.length}/${MAX_FAILED_APPROACHES}. After the third distinct approach fails, stop and summarize.`;
|
|
608
|
-
return {
|
|
609
|
-
systemPrompt: `${event.systemPrompt}\n\nHWCODE CLOUD ACTIVE\nProvider: ${details.vendor}\nObjective: ${details.request}\nCredentials must never be requested in chat, printed, placed in tool arguments, or read from the vault. Use hwcode_cloud_exec for all cloud and infrastructure operations. 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}`,
|
|
610
|
-
};
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
pi.on("tool_call", async (event, ctx) => {
|
|
614
|
-
activeState = restoreCloudWorkflow(ctx) ?? activeState;
|
|
615
|
-
if (!activeState || event.toolName !== "bash") return undefined;
|
|
616
|
-
const input = event.input as Record<string, unknown>;
|
|
617
|
-
if (typeof input.command !== "string" || !containsCloudCommand(input.command)) return undefined;
|
|
618
|
-
return {
|
|
619
|
-
block: true,
|
|
620
|
-
reason: "Direct cloud and infrastructure CLI use is blocked during HWCode Cloud. Use hwcode_cloud_exec so credentials remain isolated and approvals are enforced.",
|
|
621
|
-
};
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
pi.on("session_shutdown", async () => {
|
|
625
|
-
activeState = undefined;
|
|
626
|
-
activeCredentials = undefined;
|
|
627
|
-
cleanupCredentialDirectories();
|
|
628
|
-
});
|
|
629
|
-
}
|
|
File without changes
|