@hadooppei/hwcode 0.2.0 → 0.2.4
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/.env.example +2 -7
- package/.pi/extensions/cloud.ts +70 -28
- package/.pi/lib/cloud/template-save.ts +107 -0
- package/.pi/lib/cloud/templates.ts +126 -26
- package/.pi/lib/models/provider-config.ts +3 -3
- package/.pi/lib/workflow-guard.ts +8 -1
- package/.pi/lib/workflows/state.ts +53 -1
- package/.pi/lib/workspace/access-policy.ts +2 -2
- package/.pi/model-providers.json +2 -50
- package/README.md +44 -47
- package/bin/hwcode.js +1 -1
- package/package.json +2 -2
package/.env.example
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
# Keep real credentials outside version control.
|
|
2
|
-
#
|
|
3
|
-
# .pi/model-providers.json.
|
|
4
|
-
PI_LOCAL_MODEL_API_KEY=local
|
|
5
|
-
|
|
6
|
-
# Optional credential used as the default when logging in to the configurable
|
|
7
|
-
# "hw" provider. Leave unset for a keyless local endpoint.
|
|
1
|
+
# Keep real credentials outside version control. Set this value to provide the
|
|
2
|
+
# required "hw" credential through the environment, or enter it through /login.
|
|
8
3
|
# HW_API_KEY=
|
|
9
4
|
|
|
10
5
|
# OPENAI_API_KEY=
|
package/.pi/extensions/cloud.ts
CHANGED
|
@@ -8,7 +8,11 @@ import { CURSOR_MARKER, Input, type Component, type TUI } from "@earendil-works/
|
|
|
8
8
|
|
|
9
9
|
import { checkProviderCli, formatValidationFailure, prepareCloudExecution, validateCloudCredentials, type TemporaryCredentialStore } from "../lib/cloud/adapters.ts";
|
|
10
10
|
import { runProcess, truncateOutput, type ProcessResult } from "../lib/cloud/process.ts";
|
|
11
|
-
import {
|
|
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";
|
|
12
16
|
import {
|
|
13
17
|
CLOUD_EXECUTABLES, CLOUD_PROVIDERS, classifyCloudOperation, containsCloudCommand,
|
|
14
18
|
getCloudProvider, inaccessibleCloudCliMessage, missingCloudCliMessage, redactCredentialValues,
|
|
@@ -22,7 +26,7 @@ import {
|
|
|
22
26
|
import { canonicalizeWorkspaceRoot } from "../lib/workflow-guard.ts";
|
|
23
27
|
import { modelConfigurationIssue } from "../lib/models/readiness.ts";
|
|
24
28
|
import {
|
|
25
|
-
WORKFLOW_EXTERNAL_AUDIT_TYPE, WORKFLOW_STATE_TYPE, activeWorkflow, updateWorkflowState,
|
|
29
|
+
WORKFLOW_EXTERNAL_AUDIT_TYPE, WORKFLOW_STATE_TYPE, activeWorkflow, createCloudWorkflowState, updateWorkflowState,
|
|
26
30
|
type CloudWorkflowDetails, type WorkflowState,
|
|
27
31
|
} from "../lib/workflows/state.ts";
|
|
28
32
|
import { evaluateCommandArgumentsAccess } from "../lib/workspace/access-policy.ts";
|
|
@@ -82,6 +86,13 @@ function cloudDetails(state: WorkflowState): CloudWorkflowDetails | undefined {
|
|
|
82
86
|
return state.mode === "cloud" ? state.details : undefined;
|
|
83
87
|
}
|
|
84
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
|
+
|
|
85
96
|
function restoreCloudWorkflow(ctx: ExtensionContext): WorkflowState | undefined {
|
|
86
97
|
const state = activeWorkflow(ctx.sessionManager.getEntries());
|
|
87
98
|
return state?.mode === "cloud" && state.details ? state : undefined;
|
|
@@ -121,18 +132,6 @@ function appendWorkflowState(pi: ExtensionAPI, state: WorkflowState): void {
|
|
|
121
132
|
pi.appendEntry<WorkflowState>(WORKFLOW_STATE_TYPE, state);
|
|
122
133
|
}
|
|
123
134
|
|
|
124
|
-
function createCloudWorkflowState(root: string, vendor: CloudVendorId, deploy: boolean, request: string): WorkflowState {
|
|
125
|
-
const now = new Date().toISOString();
|
|
126
|
-
return {
|
|
127
|
-
version: 2, status: "active", mode: "cloud", root, phase: "connected",
|
|
128
|
-
activatedAt: now, updatedAt: now,
|
|
129
|
-
details: {
|
|
130
|
-
vendor, deployCurrentProject: deploy, request, allowNonDeleteChanges: false,
|
|
131
|
-
failedApproaches: [], successfulSteps: [], terminalFailure: false,
|
|
132
|
-
},
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
135
|
async function secretInput(ctx: ExtensionContext, title: string): Promise<string | undefined> {
|
|
137
136
|
if (ctx.mode !== "tui") return undefined;
|
|
138
137
|
return ctx.ui.custom<string | undefined>((tui, theme, _keybindings, done) => new MaskedInput(tui, theme, title, done));
|
|
@@ -190,7 +189,10 @@ async function collectCredentials(vendor: CloudVendorId, root: string, ctx: Exte
|
|
|
190
189
|
|
|
191
190
|
function activationPrompt(state: WorkflowState): string {
|
|
192
191
|
const details = cloudDetails(state)!;
|
|
193
|
-
|
|
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.`;
|
|
194
196
|
}
|
|
195
197
|
|
|
196
198
|
async function approveOperation(operation: CloudOperation, params: { command: string; args: string[]; intent: string }, state: WorkflowState, ctx: ExtensionContext): Promise<"allow" | "allow-session" | "deny"> {
|
|
@@ -263,11 +265,13 @@ export default function cloudExtension(pi: ExtensionAPI) {
|
|
|
263
265
|
notify(ctx, "还没有本地 Cloud Prompt Template。成功执行任务后使用 /hwcode-cloud-save-template 保存。", "warning");
|
|
264
266
|
return undefined;
|
|
265
267
|
}
|
|
268
|
+
const labels = templates.map((template) => `${template.name} · ${formatTemplateTime(template.updatedAt)}`);
|
|
266
269
|
const selected = await ctx.ui.select(
|
|
267
270
|
"选择已验证的 Cloud Prompt Template",
|
|
268
|
-
|
|
271
|
+
labels,
|
|
269
272
|
);
|
|
270
|
-
|
|
273
|
+
const selectedIndex = selected ? labels.indexOf(selected) : -1;
|
|
274
|
+
return selectedIndex >= 0 ? templates[selectedIndex] : undefined;
|
|
271
275
|
}
|
|
272
276
|
|
|
273
277
|
async function activate(args: string, ctx: ExtensionCommandContext, initialTemplate?: CloudPromptTemplate): Promise<void> {
|
|
@@ -307,6 +311,7 @@ export default function cloudExtension(pi: ExtensionAPI) {
|
|
|
307
311
|
let vendor: CloudVendorId;
|
|
308
312
|
let deployCurrentProject: boolean;
|
|
309
313
|
let request: string;
|
|
314
|
+
let templateGuidance: string | undefined;
|
|
310
315
|
if (resumedState) {
|
|
311
316
|
const details = cloudDetails(resumedState)!;
|
|
312
317
|
vendor = details.vendor;
|
|
@@ -315,7 +320,8 @@ export default function cloudExtension(pi: ExtensionAPI) {
|
|
|
315
320
|
} else if (template) {
|
|
316
321
|
vendor = template.vendor;
|
|
317
322
|
deployCurrentProject = template.deployCurrentProject;
|
|
318
|
-
request =
|
|
323
|
+
request = template.objective || template.name;
|
|
324
|
+
templateGuidance = expandCloudPromptTemplate(template, args);
|
|
319
325
|
} else {
|
|
320
326
|
const label = await ctx.ui.select("选择需要对接的云计算厂商", CLOUD_PROVIDERS.map((provider) => provider.label));
|
|
321
327
|
if (!label) return;
|
|
@@ -378,7 +384,16 @@ export default function cloudExtension(pi: ExtensionAPI) {
|
|
|
378
384
|
);
|
|
379
385
|
cleanupCredentialDirectories();
|
|
380
386
|
activeCredentials = credentials;
|
|
381
|
-
activeState = resumedState ?? createCloudWorkflowState(
|
|
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
|
+
);
|
|
382
397
|
appendWorkflowState(pi, activeState);
|
|
383
398
|
notify(ctx, `${vendorLabel} 连接成功。凭据已加密保存,项目根目录锁定为 ${root}。`);
|
|
384
399
|
pi.sendUserMessage(activationPrompt(activeState), { expandPromptTemplates: true });
|
|
@@ -404,15 +419,42 @@ export default function cloudExtension(pi: ExtensionAPI) {
|
|
|
404
419
|
notify(ctx, "当前 Cloud workflow 还没有成功执行步骤,无法生成可复用模板。", "warning");
|
|
405
420
|
return;
|
|
406
421
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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 可立即复用。`);
|
|
416
458
|
},
|
|
417
459
|
});
|
|
418
460
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { CloudPromptTemplate } from "./templates.ts";
|
|
2
|
+
import { cloudPromptTemplateSource } from "./templates.ts";
|
|
3
|
+
import type { CloudWorkflowDetails, WorkflowState } from "../workflows/state.ts";
|
|
4
|
+
|
|
5
|
+
export type CloudTemplateSourceAction = "update" | "create" | "cancel";
|
|
6
|
+
export type CloudTemplateSaveStage = "source-action" | "name" | "notes";
|
|
7
|
+
|
|
8
|
+
export interface CloudTemplateSaveInteraction {
|
|
9
|
+
chooseSourceAction(source: CloudPromptTemplate): Promise<CloudTemplateSourceAction | undefined>;
|
|
10
|
+
inputName(defaultName: string): Promise<string | undefined>;
|
|
11
|
+
editNotes(initialNotes: string): Promise<string | undefined>;
|
|
12
|
+
sourceMissing(sourceName: string): void | Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CloudTemplateStore {
|
|
16
|
+
list(): CloudPromptTemplate[];
|
|
17
|
+
create(name: string, state: WorkflowState, notes: string): CloudPromptTemplate;
|
|
18
|
+
update(template: CloudPromptTemplate, state: WorkflowState, notes: string): CloudPromptTemplate;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type CloudTemplateSaveResult =
|
|
22
|
+
| { status: "cancelled"; stage: CloudTemplateSaveStage }
|
|
23
|
+
| {
|
|
24
|
+
status: "saved";
|
|
25
|
+
action: "created" | "updated";
|
|
26
|
+
template: CloudPromptTemplate;
|
|
27
|
+
details: CloudWorkflowDetails;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export interface SaveCloudWorkflowTemplateOptions {
|
|
31
|
+
state: WorkflowState;
|
|
32
|
+
requestedName?: string;
|
|
33
|
+
interaction: CloudTemplateSaveInteraction;
|
|
34
|
+
store: CloudTemplateStore;
|
|
35
|
+
redact?: (value: string) => string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Coordinates the save/update decision without depending on Pi UI or filesystem APIs.
|
|
40
|
+
* Storage is invoked only after every interactive step succeeds, so cancellation and
|
|
41
|
+
* storage failures cannot partially change the workflow state.
|
|
42
|
+
*/
|
|
43
|
+
export async function saveCloudWorkflowTemplate(
|
|
44
|
+
options: SaveCloudWorkflowTemplateOptions,
|
|
45
|
+
): Promise<CloudTemplateSaveResult> {
|
|
46
|
+
const { state, interaction, store } = options;
|
|
47
|
+
if (state.mode !== "cloud" || !state.details) throw new Error("Cloud workflow state is required");
|
|
48
|
+
if (state.details.successfulSteps.length === 0) {
|
|
49
|
+
throw new Error("No successful Cloud execution steps are available to summarize");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const details = state.details;
|
|
53
|
+
let target: CloudPromptTemplate | undefined;
|
|
54
|
+
if (details.sourceTemplate) {
|
|
55
|
+
target = store.list().find((template) => template.id === details.sourceTemplate?.id);
|
|
56
|
+
if (target) {
|
|
57
|
+
const sourceAction = await interaction.chooseSourceAction(target);
|
|
58
|
+
if (!sourceAction || sourceAction === "cancel") {
|
|
59
|
+
return { status: "cancelled", stage: "source-action" };
|
|
60
|
+
}
|
|
61
|
+
if (sourceAction === "create") target = undefined;
|
|
62
|
+
} else {
|
|
63
|
+
await interaction.sourceMissing(details.sourceTemplate.name);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let name = target?.name;
|
|
68
|
+
if (!name) {
|
|
69
|
+
const suppliedName = options.requestedName?.trim();
|
|
70
|
+
if (suppliedName) {
|
|
71
|
+
name = suppliedName;
|
|
72
|
+
} else {
|
|
73
|
+
const defaultName = details.sourceTemplate
|
|
74
|
+
? `${details.sourceTemplate.name} 副本`
|
|
75
|
+
: details.request.slice(0, 48);
|
|
76
|
+
name = (await interaction.inputName(defaultName))?.trim();
|
|
77
|
+
}
|
|
78
|
+
if (!name) return { status: "cancelled", stage: "name" };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const notes = await interaction.editNotes(target?.notes ?? "");
|
|
82
|
+
if (notes === undefined) return { status: "cancelled", stage: "notes" };
|
|
83
|
+
|
|
84
|
+
const redact = options.redact ?? ((value: string) => value);
|
|
85
|
+
const safeName = redact(name);
|
|
86
|
+
const safeNotes = redact(notes.trim());
|
|
87
|
+
const safeState: WorkflowState = {
|
|
88
|
+
...state,
|
|
89
|
+
details: {
|
|
90
|
+
...details,
|
|
91
|
+
request: redact(details.request),
|
|
92
|
+
templateGuidance: details.templateGuidance === undefined
|
|
93
|
+
? undefined
|
|
94
|
+
: redact(details.templateGuidance),
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
const template = target
|
|
98
|
+
? store.update(target, safeState, safeNotes)
|
|
99
|
+
: store.create(safeName, safeState, safeNotes);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
status: "saved",
|
|
103
|
+
action: target ? "updated" : "created",
|
|
104
|
+
template,
|
|
105
|
+
details: { ...details, sourceTemplate: cloudPromptTemplateSource(template) },
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -1,20 +1,33 @@
|
|
|
1
|
-
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { basename, join } from "node:path";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
4
|
|
|
5
5
|
import { getCloudProvider, isCloudVendorId, type CloudVendorId } from "../cloud-providers.ts";
|
|
6
|
-
import type { WorkflowState } from "../workflows/state.ts";
|
|
6
|
+
import type { CloudTemplateSource, WorkflowState } from "../workflows/state.ts";
|
|
7
7
|
|
|
8
8
|
export interface CloudPromptTemplate {
|
|
9
9
|
id: string;
|
|
10
|
+
name: string;
|
|
10
11
|
description: string;
|
|
12
|
+
createdAt: string;
|
|
13
|
+
updatedAt: string;
|
|
11
14
|
vendor: CloudVendorId;
|
|
12
15
|
deployCurrentProject: boolean;
|
|
13
16
|
objective: string;
|
|
17
|
+
notes: string;
|
|
14
18
|
body: string;
|
|
15
19
|
path: string;
|
|
16
20
|
}
|
|
17
21
|
|
|
22
|
+
export function cloudPromptTemplateSource(template: CloudPromptTemplate): CloudTemplateSource {
|
|
23
|
+
return {
|
|
24
|
+
id: template.id,
|
|
25
|
+
name: template.name,
|
|
26
|
+
createdAt: template.createdAt,
|
|
27
|
+
updatedAt: template.updatedAt,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
18
31
|
export function defaultCloudPromptDirectory(home = homedir()): string {
|
|
19
32
|
return join(home, ".hwcode", "cloud", "prompts");
|
|
20
33
|
}
|
|
@@ -40,6 +53,22 @@ function frontmatterValue(text: string, key: string): string | undefined {
|
|
|
40
53
|
}
|
|
41
54
|
}
|
|
42
55
|
|
|
56
|
+
function normalizeTimestamp(value: string | undefined, fallback: string): string {
|
|
57
|
+
if (!value) return fallback;
|
|
58
|
+
const date = new Date(value);
|
|
59
|
+
return Number.isNaN(date.valueOf()) ? fallback : date.toISOString();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function legacyTemplateName(id: string): string {
|
|
63
|
+
const words = id.replace(/^hwcloud-/u, "").split("-").filter(Boolean);
|
|
64
|
+
if (words.length === 0) return "Legacy Cloud Template";
|
|
65
|
+
return words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function cleanTemplateName(value: string): string {
|
|
69
|
+
return value.replace(/[\r\n]+/gu, " ").replace(/\s+/gu, " ").trim().slice(0, 80);
|
|
70
|
+
}
|
|
71
|
+
|
|
43
72
|
export function parseCloudPromptTemplate(path: string): CloudPromptTemplate | undefined {
|
|
44
73
|
const text = readFileSync(path, "utf8");
|
|
45
74
|
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/u);
|
|
@@ -48,12 +77,20 @@ export function parseCloudPromptTemplate(path: string): CloudPromptTemplate | un
|
|
|
48
77
|
const vendorValue = frontmatterValue(metadata, "hwcode-cloud-vendor");
|
|
49
78
|
if (!vendorValue || !isCloudVendorId(vendorValue)) return undefined;
|
|
50
79
|
const id = basename(path, ".md");
|
|
80
|
+
const stats = statSync(path);
|
|
81
|
+
const modifiedAt = stats.mtime.toISOString();
|
|
82
|
+
const bornAt = stats.birthtimeMs > 0 ? stats.birthtime.toISOString() : modifiedAt;
|
|
83
|
+
const name = cleanTemplateName(frontmatterValue(metadata, "hwcode-cloud-name") ?? "") || legacyTemplateName(id);
|
|
51
84
|
return {
|
|
52
85
|
id,
|
|
53
|
-
|
|
86
|
+
name,
|
|
87
|
+
description: frontmatterValue(metadata, "description") ?? name,
|
|
88
|
+
createdAt: normalizeTimestamp(frontmatterValue(metadata, "hwcode-cloud-created-at"), bornAt),
|
|
89
|
+
updatedAt: normalizeTimestamp(frontmatterValue(metadata, "hwcode-cloud-updated-at"), modifiedAt),
|
|
54
90
|
vendor: vendorValue,
|
|
55
91
|
deployCurrentProject: frontmatterValue(metadata, "hwcode-cloud-deploy") === "true",
|
|
56
92
|
objective: frontmatterValue(metadata, "hwcode-cloud-objective") ?? "",
|
|
93
|
+
notes: frontmatterValue(metadata, "hwcode-cloud-notes") ?? "",
|
|
57
94
|
body: match[2].trim(),
|
|
58
95
|
path,
|
|
59
96
|
};
|
|
@@ -67,7 +104,7 @@ export function listCloudPromptTemplates(
|
|
|
67
104
|
.filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
|
|
68
105
|
.map((entry) => parseCloudPromptTemplate(join(directory, entry.name)))
|
|
69
106
|
.filter((template): template is CloudPromptTemplate => Boolean(template))
|
|
70
|
-
.sort((left, right) => left.
|
|
107
|
+
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt) || left.name.localeCompare(right.name));
|
|
71
108
|
}
|
|
72
109
|
|
|
73
110
|
export function expandCloudPromptTemplate(template: CloudPromptTemplate, argumentsText: string): string {
|
|
@@ -79,6 +116,10 @@ function inlineCode(value: string): string {
|
|
|
79
116
|
return value.replace(/[\r\n]+/gu, " ").replaceAll("`", "\\`");
|
|
80
117
|
}
|
|
81
118
|
|
|
119
|
+
function singleLine(value: string): string {
|
|
120
|
+
return value.replace(/[\r\n]+/gu, " ").replace(/\s+/gu, " ").trim();
|
|
121
|
+
}
|
|
122
|
+
|
|
82
123
|
export function renderCloudPromptTemplate(state: WorkflowState, notes = ""): string {
|
|
83
124
|
if (state.mode !== "cloud" || !state.details) throw new Error("Cloud workflow state is required");
|
|
84
125
|
const details = state.details;
|
|
@@ -87,62 +128,121 @@ export function renderCloudPromptTemplate(state: WorkflowState, notes = ""): str
|
|
|
87
128
|
}
|
|
88
129
|
const provider = getCloudProvider(details.vendor);
|
|
89
130
|
const steps = details.successfulSteps.map((step, index) => (
|
|
90
|
-
`${index + 1}. [${step.operation}] ${step.intent}\n`
|
|
91
|
-
+ ` -
|
|
92
|
-
+ ` -
|
|
131
|
+
`${index + 1}. [${step.operation}] ${singleLine(step.intent)}\n`
|
|
132
|
+
+ ` - Validated approach: ${singleLine(step.approach)}\n`
|
|
133
|
+
+ ` - Known-good command and path: \`${inlineCode([step.command, ...step.args].join(" "))}\``
|
|
93
134
|
)).join("\n");
|
|
135
|
+
const failedPaths = details.failedApproaches.length > 0
|
|
136
|
+
? details.failedApproaches.map((failure, index) => (
|
|
137
|
+
`${index + 1}. **DO NOT RETRY:** ${singleLine(failure.approach)}\n`
|
|
138
|
+
+ ` - Recorded failure: ${singleLine(failure.reason)}`
|
|
139
|
+
)).join("\n")
|
|
140
|
+
: "No failed execution paths were recorded for this run.";
|
|
94
141
|
return [
|
|
95
|
-
"Execute
|
|
142
|
+
"Execute this HWCode Cloud task from a layered record of a previously successful run.",
|
|
143
|
+
"",
|
|
144
|
+
"## Layer 1 — Stable intent and boundaries",
|
|
96
145
|
"",
|
|
97
146
|
`Preferred provider: ${provider.label}`,
|
|
98
147
|
`Deploy current project: ${details.deployCurrentProject ? "yes" : "no"}`,
|
|
99
148
|
`Objective pattern: ${details.request}`,
|
|
100
149
|
"",
|
|
101
|
-
"
|
|
150
|
+
"Preserve the objective, provider boundary, credential isolation, resource approvals, and deletion confirmations. Adapt only environment-specific values.",
|
|
151
|
+
"",
|
|
152
|
+
"## Layer 2 — Preconditions and operator knowledge",
|
|
153
|
+
"",
|
|
154
|
+
"Start with read-only discovery. Re-check the active account, region, project, resource state, tool versions, and workspace paths before making changes.",
|
|
155
|
+
...(notes ? ["", "Validated prerequisites, lessons, and expensive pitfalls:", notes] : []),
|
|
156
|
+
"",
|
|
157
|
+
"## Layer 3 — Preferred validated execution path",
|
|
158
|
+
"",
|
|
159
|
+
"Use this path first and preserve its order unless read-only evidence requires an adaptation. The commands and paths below are the known-good reference; do not replace them with speculative alternatives.",
|
|
160
|
+
"",
|
|
102
161
|
steps,
|
|
103
162
|
"",
|
|
104
|
-
|
|
105
|
-
"
|
|
163
|
+
"## Layer 4 — Expensive failed paths to avoid",
|
|
164
|
+
"",
|
|
165
|
+
"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.",
|
|
166
|
+
"",
|
|
167
|
+
failedPaths,
|
|
168
|
+
"",
|
|
169
|
+
"## Layer 5 — Execution contract",
|
|
170
|
+
"",
|
|
171
|
+
"Present the adapted plan and obtain all normal HWCode Cloud approvals. Never copy credentials, assume old resources still exist, or skip current-state verification.",
|
|
106
172
|
"If HWCode Cloud is not active, do not run cloud commands directly; ask the user to start this template through /hwcode-cloud-template.",
|
|
107
173
|
"",
|
|
108
174
|
"Additional instructions: ${@:-Use the objective and successful sequence above.}",
|
|
109
175
|
].join("\n");
|
|
110
176
|
}
|
|
111
177
|
|
|
112
|
-
|
|
113
|
-
|
|
178
|
+
function renderTemplateFile(
|
|
179
|
+
name: string,
|
|
114
180
|
state: WorkflowState,
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
181
|
+
notes: string,
|
|
182
|
+
createdAt: string,
|
|
183
|
+
updatedAt: string,
|
|
184
|
+
): string {
|
|
118
185
|
if (state.mode !== "cloud" || !state.details) throw new Error("Cloud workflow state is required");
|
|
119
|
-
const idBase = `hwcloud-${slugify(requestedName || state.details.request)}`;
|
|
120
186
|
const body = renderCloudPromptTemplate(state, notes);
|
|
121
|
-
|
|
122
|
-
const content = [
|
|
187
|
+
return [
|
|
123
188
|
"---",
|
|
124
|
-
`description: ${JSON.stringify(
|
|
189
|
+
`description: ${JSON.stringify(name)}`,
|
|
125
190
|
'argument-hint: "[additional instructions]"',
|
|
191
|
+
`hwcode-cloud-name: ${JSON.stringify(name)}`,
|
|
192
|
+
`hwcode-cloud-created-at: ${JSON.stringify(createdAt)}`,
|
|
193
|
+
`hwcode-cloud-updated-at: ${JSON.stringify(updatedAt)}`,
|
|
126
194
|
`hwcode-cloud-vendor: ${JSON.stringify(state.details.vendor)}`,
|
|
127
195
|
`hwcode-cloud-deploy: ${state.details.deployCurrentProject}`,
|
|
128
196
|
`hwcode-cloud-objective: ${JSON.stringify(state.details.request)}`,
|
|
197
|
+
`hwcode-cloud-notes: ${JSON.stringify(notes)}`,
|
|
129
198
|
"---",
|
|
130
199
|
body,
|
|
131
200
|
"",
|
|
132
201
|
].join("\n");
|
|
202
|
+
}
|
|
133
203
|
|
|
204
|
+
function writeTemplateFile(path: string, content: string): void {
|
|
205
|
+
const directory = dirname(path);
|
|
134
206
|
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
135
207
|
chmodSync(directory, 0o700);
|
|
208
|
+
const temporaryPath = `${path}.${process.pid}.tmp`;
|
|
209
|
+
writeFileSync(temporaryPath, content, { encoding: "utf8", mode: 0o600 });
|
|
210
|
+
chmodSync(temporaryPath, 0o600);
|
|
211
|
+
renameSync(temporaryPath, path);
|
|
212
|
+
chmodSync(path, 0o600);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function saveCloudPromptTemplate(
|
|
216
|
+
requestedName: string,
|
|
217
|
+
state: WorkflowState,
|
|
218
|
+
directory = defaultCloudPromptDirectory(),
|
|
219
|
+
notes = "",
|
|
220
|
+
savedAt = new Date(),
|
|
221
|
+
): CloudPromptTemplate {
|
|
222
|
+
if (state.mode !== "cloud" || !state.details) throw new Error("Cloud workflow state is required");
|
|
223
|
+
const name = cleanTemplateName(requestedName);
|
|
224
|
+
if (!name) throw new Error("Cloud Prompt Template name is required");
|
|
225
|
+
const idBase = `hwcloud-${slugify(name)}`;
|
|
226
|
+
const timestamp = savedAt.toISOString();
|
|
227
|
+
const content = renderTemplateFile(name, state, notes, timestamp, timestamp);
|
|
136
228
|
let id = idBase;
|
|
137
229
|
let path = join(directory, `${id}.md`);
|
|
138
230
|
for (let suffix = 2; existsSync(path); suffix++) {
|
|
139
231
|
id = `${idBase}-${suffix}`;
|
|
140
232
|
path = join(directory, `${id}.md`);
|
|
141
233
|
}
|
|
142
|
-
|
|
143
|
-
writeFileSync(temporaryPath, content, { encoding: "utf8", mode: 0o600 });
|
|
144
|
-
chmodSync(temporaryPath, 0o600);
|
|
145
|
-
renameSync(temporaryPath, path);
|
|
146
|
-
chmodSync(path, 0o600);
|
|
234
|
+
writeTemplateFile(path, content);
|
|
147
235
|
return parseCloudPromptTemplate(path)!;
|
|
148
236
|
}
|
|
237
|
+
|
|
238
|
+
export function updateCloudPromptTemplate(
|
|
239
|
+
template: CloudPromptTemplate,
|
|
240
|
+
state: WorkflowState,
|
|
241
|
+
notes = template.notes,
|
|
242
|
+
updatedAt = new Date(),
|
|
243
|
+
): CloudPromptTemplate {
|
|
244
|
+
const timestamp = updatedAt.toISOString();
|
|
245
|
+
const content = renderTemplateFile(template.name, state, notes, template.createdAt, timestamp);
|
|
246
|
+
writeTemplateFile(template.path, content);
|
|
247
|
+
return parseCloudPromptTemplate(template.path)!;
|
|
248
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { dirname, join } from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { readJsonObject } from "../runtime/config.ts";
|
|
5
5
|
|
|
6
6
|
export type ModelInput = "text" | "image";
|
|
7
7
|
export interface ConfiguredModel { id: string; name?: string; reasoning?: boolean; input?: ModelInput[]; contextWindow?: number; maxTokens?: number; }
|
|
@@ -42,9 +42,9 @@ function validateModel(providerId: string, model: ConfiguredModel): void {
|
|
|
42
42
|
if (model.maxTokens !== undefined && !isPositiveNumber(model.maxTokens)) throw new Error(`Model "${model.id}" has an invalid maxTokens`);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
export function loadModelProvidersConfig(
|
|
45
|
+
export function loadModelProvidersConfig(): ModelProvidersConfig {
|
|
46
46
|
const source = "model-providers.json";
|
|
47
|
-
const config =
|
|
47
|
+
const config = readJsonObject(BUNDLED_CONFIG_PATH) as ModelProvidersConfig & Record<string, unknown>;
|
|
48
48
|
if (!Array.isArray(config.providers) || config.providers.length === 0) throw new Error(`${source} must contain a non-empty providers array`);
|
|
49
49
|
const providerIds = new Set<string>();
|
|
50
50
|
for (const provider of config.providers) {
|
|
@@ -11,6 +11,8 @@ export interface ExternalPathReference {
|
|
|
11
11
|
resolved: string;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
const APPROVAL_FREE_EXTERNAL_PATHS = new Set(["/dev/null"]);
|
|
15
|
+
|
|
14
16
|
function realpathWithMissingTail(path: string): string {
|
|
15
17
|
const tail: string[] = [];
|
|
16
18
|
let cursor = resolve(path);
|
|
@@ -41,6 +43,11 @@ export function isPathInsideRoot(root: string, candidate: string): boolean {
|
|
|
41
43
|
return relation === "" || (!relation.startsWith(`..${sep}`) && relation !== ".." && !isAbsolute(relation));
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
export function isPathAllowedWithoutApproval(root: string, candidate: string): boolean {
|
|
47
|
+
const resolved = realpathWithMissingTail(candidate);
|
|
48
|
+
return APPROVAL_FREE_EXTERNAL_PATHS.has(resolved) || isPathInsideRoot(root, resolved);
|
|
49
|
+
}
|
|
50
|
+
|
|
44
51
|
function stripTrailingShellPunctuation(value: string): string {
|
|
45
52
|
return value.replace(/[\]}),;]+$/g, "");
|
|
46
53
|
}
|
|
@@ -52,7 +59,7 @@ function addExternalReference(
|
|
|
52
59
|
resolved: string,
|
|
53
60
|
): void {
|
|
54
61
|
const cleanResolved = realpathWithMissingTail(stripTrailingShellPunctuation(resolved));
|
|
55
|
-
if (!
|
|
62
|
+
if (!isPathAllowedWithoutApproval(root, cleanResolved)) {
|
|
56
63
|
result.set(`${raw}\0${cleanResolved}`, { raw, resolved: cleanResolved });
|
|
57
64
|
}
|
|
58
65
|
}
|
|
@@ -21,6 +21,13 @@ export interface CloudExecutionStep {
|
|
|
21
21
|
completedAt: string;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
export interface CloudTemplateSource {
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
createdAt: string;
|
|
28
|
+
updatedAt: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
24
31
|
export interface CloudWorkflowDetails {
|
|
25
32
|
vendor: "aws" | "azure" | "gcp" | "huawei" | "alibaba" | "tencent";
|
|
26
33
|
deployCurrentProject: boolean;
|
|
@@ -29,6 +36,8 @@ export interface CloudWorkflowDetails {
|
|
|
29
36
|
failedApproaches: FailedApproach[];
|
|
30
37
|
successfulSteps: CloudExecutionStep[];
|
|
31
38
|
terminalFailure: boolean;
|
|
39
|
+
templateGuidance?: string;
|
|
40
|
+
sourceTemplate?: CloudTemplateSource;
|
|
32
41
|
}
|
|
33
42
|
|
|
34
43
|
export interface WorkflowState {
|
|
@@ -52,6 +61,15 @@ interface LegacyWorkflowState {
|
|
|
52
61
|
deactivatedAt?: string;
|
|
53
62
|
}
|
|
54
63
|
|
|
64
|
+
function isCloudTemplateSource(value: unknown): value is CloudTemplateSource {
|
|
65
|
+
if (!value || typeof value !== "object") return false;
|
|
66
|
+
const data = value as Record<string, unknown>;
|
|
67
|
+
return typeof data.id === "string"
|
|
68
|
+
&& typeof data.name === "string"
|
|
69
|
+
&& typeof data.createdAt === "string"
|
|
70
|
+
&& typeof data.updatedAt === "string";
|
|
71
|
+
}
|
|
72
|
+
|
|
55
73
|
function isCloudDetails(value: unknown): value is CloudWorkflowDetails {
|
|
56
74
|
if (!value || typeof value !== "object") return false;
|
|
57
75
|
const data = value as Record<string, unknown>;
|
|
@@ -61,7 +79,9 @@ function isCloudDetails(value: unknown): value is CloudWorkflowDetails {
|
|
|
61
79
|
&& typeof data.allowNonDeleteChanges === "boolean"
|
|
62
80
|
&& Array.isArray(data.failedApproaches)
|
|
63
81
|
&& Array.isArray(data.successfulSteps)
|
|
64
|
-
&& typeof data.terminalFailure === "boolean"
|
|
82
|
+
&& typeof data.terminalFailure === "boolean"
|
|
83
|
+
&& (data.templateGuidance === undefined || typeof data.templateGuidance === "string")
|
|
84
|
+
&& (data.sourceTemplate === undefined || isCloudTemplateSource(data.sourceTemplate));
|
|
65
85
|
}
|
|
66
86
|
|
|
67
87
|
export function decodeWorkflowState(value: unknown): WorkflowState | undefined {
|
|
@@ -124,6 +144,38 @@ export function createWorkflowState(
|
|
|
124
144
|
};
|
|
125
145
|
}
|
|
126
146
|
|
|
147
|
+
export function createCloudWorkflowState(
|
|
148
|
+
root: string,
|
|
149
|
+
vendor: CloudWorkflowDetails["vendor"],
|
|
150
|
+
deployCurrentProject: boolean,
|
|
151
|
+
request: string,
|
|
152
|
+
template?: { source: CloudTemplateSource; guidance: string },
|
|
153
|
+
): WorkflowState {
|
|
154
|
+
const now = new Date().toISOString();
|
|
155
|
+
return {
|
|
156
|
+
version: 2,
|
|
157
|
+
status: "active",
|
|
158
|
+
mode: "cloud",
|
|
159
|
+
root,
|
|
160
|
+
phase: "connected",
|
|
161
|
+
activatedAt: now,
|
|
162
|
+
updatedAt: now,
|
|
163
|
+
details: {
|
|
164
|
+
vendor,
|
|
165
|
+
deployCurrentProject,
|
|
166
|
+
request,
|
|
167
|
+
allowNonDeleteChanges: false,
|
|
168
|
+
failedApproaches: [],
|
|
169
|
+
successfulSteps: [],
|
|
170
|
+
terminalFailure: false,
|
|
171
|
+
...(template ? {
|
|
172
|
+
templateGuidance: template.guidance,
|
|
173
|
+
sourceTemplate: template.source,
|
|
174
|
+
} : {}),
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
127
179
|
export function updateWorkflowState(
|
|
128
180
|
state: WorkflowState,
|
|
129
181
|
changes: Partial<Omit<WorkflowState, "version" | "mode" | "root" | "activatedAt">>,
|
|
@@ -2,7 +2,7 @@ import { homedir, tmpdir } from "node:os";
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
findExternalPathReferences,
|
|
5
|
-
|
|
5
|
+
isPathAllowedWithoutApproval,
|
|
6
6
|
resolveToolPath,
|
|
7
7
|
type ExternalPathReference,
|
|
8
8
|
} from "../workflow-guard.ts";
|
|
@@ -21,7 +21,7 @@ export function evaluateToolPathAccess(
|
|
|
21
21
|
): ExternalPathReference[] {
|
|
22
22
|
if (FILE_PATH_TOOLS.has(request.toolName) && typeof request.input.path === "string") {
|
|
23
23
|
const resolved = resolveToolPath(root, request.input.path, homedir());
|
|
24
|
-
return
|
|
24
|
+
return isPathAllowedWithoutApproval(root, resolved)
|
|
25
25
|
? []
|
|
26
26
|
: [{ raw: request.input.path, resolved }];
|
|
27
27
|
}
|
package/.pi/model-providers.json
CHANGED
|
@@ -1,53 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"providers": [
|
|
3
|
-
{
|
|
4
|
-
"id": "local",
|
|
5
|
-
"name": "Qwen3 VL (8081)",
|
|
6
|
-
"baseUrl": "http://127.0.0.1:8081",
|
|
7
|
-
"apiKeyEnv": "PI_LOCAL_MODEL_API_KEY",
|
|
8
|
-
"modelDefaults": {
|
|
9
|
-
"contextWindow": 1000000,
|
|
10
|
-
"maxTokens": 8192
|
|
11
|
-
},
|
|
12
|
-
"models": [
|
|
13
|
-
{
|
|
14
|
-
"id": "qwen3-VL:2b",
|
|
15
|
-
"name": "Qwen3 VL 2B",
|
|
16
|
-
"input": ["text", "image"],
|
|
17
|
-
"contextWindow": 1000000,
|
|
18
|
-
"maxTokens": 8192
|
|
19
|
-
}
|
|
20
|
-
]
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
"id": "local-8080",
|
|
24
|
-
"name": "Qwen3.5 9B (8080)",
|
|
25
|
-
"baseUrl": "http://127.0.0.1:8080",
|
|
26
|
-
"apiKeyEnv": "PI_LOCAL_MODEL_API_KEY",
|
|
27
|
-
"modelDefaults": {
|
|
28
|
-
"contextWindow": 1000000,
|
|
29
|
-
"maxTokens": 8192
|
|
30
|
-
},
|
|
31
|
-
"models": [
|
|
32
|
-
{
|
|
33
|
-
"id": "Qwen3.5-9B-Q4_K_M",
|
|
34
|
-
"name": "Qwen3.5 9B Q4_K_M",
|
|
35
|
-
"input": ["text"],
|
|
36
|
-
"contextWindow": 1000000,
|
|
37
|
-
"maxTokens": 8192
|
|
38
|
-
}
|
|
39
|
-
]
|
|
40
|
-
},
|
|
41
3
|
{
|
|
42
4
|
"id": "hw",
|
|
43
5
|
"name": "Huawei MaaS",
|
|
44
|
-
"baseUrl": "http://127.0.0.1:8080/v1",
|
|
45
6
|
"apiKeyEnv": "HW_API_KEY",
|
|
46
7
|
"login": {
|
|
47
8
|
"enabled": true,
|
|
48
9
|
"promptBaseUrl": true,
|
|
49
10
|
"promptApiKey": true,
|
|
50
|
-
"apiKeyRequired":
|
|
11
|
+
"apiKeyRequired": true,
|
|
51
12
|
"catalogPath": "models",
|
|
52
13
|
"timeoutMs": 15000
|
|
53
14
|
},
|
|
@@ -55,16 +16,7 @@
|
|
|
55
16
|
"input": ["text"],
|
|
56
17
|
"contextWindow": 1000000,
|
|
57
18
|
"maxTokens": 8192
|
|
58
|
-
}
|
|
59
|
-
"models": [
|
|
60
|
-
{
|
|
61
|
-
"id": "Qwen3.5-9B-Q4_K_M",
|
|
62
|
-
"name": "Qwen3.5 9B Q4_K_M",
|
|
63
|
-
"input": ["text"],
|
|
64
|
-
"contextWindow": 1000000,
|
|
65
|
-
"maxTokens": 8192
|
|
66
|
-
}
|
|
67
|
-
]
|
|
19
|
+
}
|
|
68
20
|
}
|
|
69
21
|
]
|
|
70
22
|
}
|
package/README.md
CHANGED
|
@@ -21,10 +21,11 @@ hwcode
|
|
|
21
21
|
Run `hwcode --help` for the HWCode command reference, including model,
|
|
22
22
|
session, tool, and workflow options.
|
|
23
23
|
|
|
24
|
-
`npx @hadooppei/hwcode` is also supported without a global installation. HWCode
|
|
25
|
-
project's `.env` file when present and keeps Pi's normal user-level auth
|
|
26
|
-
session storage. Project-local
|
|
27
|
-
built-in HWCode profile
|
|
24
|
+
`npx @hadooppei/hwcode` is also supported without a global installation. HWCode
|
|
25
|
+
loads a project's `.env` file when present and keeps Pi's normal user-level auth
|
|
26
|
+
and session storage. Project-local Pi resources continue to load alongside the
|
|
27
|
+
built-in HWCode profile, except for HWCode's package-scoped model-provider
|
|
28
|
+
definition described below.
|
|
28
29
|
|
|
29
30
|
## Repository setup
|
|
30
31
|
|
|
@@ -38,45 +39,31 @@ On first launch, review and accept Pi's project-trust prompt so it can load the
|
|
|
38
39
|
|
|
39
40
|
## Model providers
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
The published `.pi/model-providers.json` is package-scoped and is not replaced
|
|
43
|
+
by a file in the current project. It registers the configurable `hw` provider
|
|
44
|
+
without shipping a machine-specific endpoint or a pre-populated model catalog.
|
|
45
|
+
Use Pi's native `/login` flow, select `Huawei MaaS`, and enter the endpoint and
|
|
46
|
+
API key. The key can instead come from `HW_API_KEY`. HWCode then discovers every model exposed by the
|
|
47
|
+
OpenAI-compatible endpoint and Pi stores the selected endpoint, credential, and
|
|
48
|
+
dynamic catalog in its user-level stores.
|
|
43
49
|
|
|
44
|
-
|
|
45
|
-
{
|
|
46
|
-
"id": "local-example",
|
|
47
|
-
"name": "Local example",
|
|
48
|
-
"baseUrl": "http://127.0.0.1:8082",
|
|
49
|
-
"apiKeyEnv": "PI_LOCAL_MODEL_API_KEY",
|
|
50
|
-
"models": [
|
|
51
|
-
{
|
|
52
|
-
"id": "model-id",
|
|
53
|
-
"input": ["text", "image"],
|
|
54
|
-
"contextWindow": 32768,
|
|
55
|
-
"maxTokens": 8192
|
|
56
|
-
}
|
|
57
|
-
]
|
|
58
|
-
}
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
Login providers use Pi's native `/login` flow and discover every model exposed
|
|
62
|
-
by an OpenAI-compatible model endpoint:
|
|
50
|
+
The published provider is equivalent to:
|
|
63
51
|
|
|
64
52
|
```json
|
|
65
53
|
{
|
|
66
54
|
"id": "hw",
|
|
67
|
-
"name": "
|
|
68
|
-
"baseUrl": "http://127.0.0.1:8080/v1",
|
|
55
|
+
"name": "Huawei MaaS",
|
|
69
56
|
"apiKeyEnv": "HW_API_KEY",
|
|
70
57
|
"login": {
|
|
71
58
|
"enabled": true,
|
|
72
59
|
"promptBaseUrl": true,
|
|
73
60
|
"promptApiKey": true,
|
|
74
|
-
"apiKeyRequired":
|
|
61
|
+
"apiKeyRequired": true,
|
|
75
62
|
"catalogPath": "models"
|
|
76
63
|
},
|
|
77
64
|
"modelDefaults": {
|
|
78
65
|
"input": ["text"],
|
|
79
|
-
"contextWindow":
|
|
66
|
+
"contextWindow": 1000000,
|
|
80
67
|
"maxTokens": 8192
|
|
81
68
|
}
|
|
82
69
|
}
|
|
@@ -84,12 +71,14 @@ by an OpenAI-compatible model endpoint:
|
|
|
84
71
|
|
|
85
72
|
`id` is the stable credential and cache key; `name` is the configurable label
|
|
86
73
|
shown alongside other providers in `/login`. A login authenticates the whole
|
|
87
|
-
provider and publishes its complete model catalog to `/model`.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
74
|
+
provider and publishes its complete model catalog to `/model`. Unknown models
|
|
75
|
+
default to text-only. Keep real API keys in `.env` or enter them through
|
|
76
|
+
`/login`; Pi stores entered credentials in its own auth store rather than the
|
|
77
|
+
package configuration.
|
|
78
|
+
|
|
79
|
+
Repository maintainers can refer to `.pi/model-providers.json.template` for the
|
|
80
|
+
previous local static-provider examples, including multimodal metadata. The
|
|
81
|
+
template is not loaded at runtime and is explicitly excluded from npm packages.
|
|
93
82
|
|
|
94
83
|
## Welcome screen
|
|
95
84
|
|
|
@@ -111,8 +100,9 @@ Start one of the project workflows from the Pi input:
|
|
|
111
100
|
|
|
112
101
|
Both commands first confirm the current directory and lock project work to that
|
|
113
102
|
root for the session. In-root operations run normally; each tool call that names
|
|
114
|
-
an external path asks for separate approval.
|
|
115
|
-
|
|
103
|
+
an external path asks for separate approval. The non-storage sink `/dev/null` is
|
|
104
|
+
approval-free even when used by shell redirection. `/hwcode-vibe` uses short
|
|
105
|
+
iterative build-and-verify loops. `/hwcode-sdd` additionally requires the current directory
|
|
116
106
|
to be the Git repository root, inventories the codebase, resolves requirement
|
|
117
107
|
questions, and persists approved artifacts under
|
|
118
108
|
`.hwcode/specs/<requirement-slug>/` before test-first implementation begins.
|
|
@@ -149,13 +139,19 @@ remaining resources, and local changes instead of attempting a fourth approach.
|
|
|
149
139
|
|
|
150
140
|
After at least one cloud command succeeds, run
|
|
151
141
|
`/hwcode-cloud-save-template [name]` to save the objective, validated execution
|
|
152
|
-
sequence, and optional lessons learned as a
|
|
153
|
-
|
|
154
|
-
|
|
142
|
+
sequence, failed approaches, prerequisites, and optional lessons learned as a
|
|
143
|
+
local Prompt Template. Each template records its user-facing name plus creation
|
|
144
|
+
and update timestamps. Its prompt is layered into stable intent, prerequisites,
|
|
145
|
+
the preferred known-good path, expensive failed paths that must not be retried,
|
|
146
|
+
and the guarded execution contract. Templates are stored under
|
|
147
|
+
`~/.hwcode/cloud/prompts/` with user-only permissions. Credential values are
|
|
148
|
+
excluded and redacted before steps are persisted.
|
|
155
149
|
|
|
156
150
|
Use `/hwcode-cloud-template [additional instructions]` to select and start a
|
|
157
|
-
saved template
|
|
158
|
-
starts without an inline request.
|
|
151
|
+
saved template by name and last-updated time. `/hwcode-cloud` also offers saved
|
|
152
|
+
templates when it starts without an inline request. If a session started from a
|
|
153
|
+
template, saving again asks whether to update that source template or create a
|
|
154
|
+
new one. The files use a Pi-compatible
|
|
159
155
|
Markdown/frontmatter format, but HWCode intentionally keeps them out of Pi
|
|
160
156
|
resource discovery so each saved template does not become another slash
|
|
161
157
|
command. Templates are extension-private resources and always run through the
|
|
@@ -174,9 +170,9 @@ commands, tools, and UI. Reusable behavior lives under `.pi/lib/`:
|
|
|
174
170
|
- `context/` and `models/` own compaction and provider-configuration policy.
|
|
175
171
|
|
|
176
172
|
`settings.json` is layered as defaults → profile → project for settings such as
|
|
177
|
-
context and hidden commands. `welcome.json`
|
|
178
|
-
|
|
179
|
-
|
|
173
|
+
context and hidden commands. `welcome.json` uses a single replacing resource.
|
|
174
|
+
`model-providers.json` is deliberately package-scoped so a project cannot
|
|
175
|
+
silently replace login providers or reintroduce machine-local defaults.
|
|
180
176
|
|
|
181
177
|
## Working directory
|
|
182
178
|
|
|
@@ -204,8 +200,9 @@ HWCode defaults locally configured and dynamically discovered models to a
|
|
|
204
200
|
1,000,000-token context window and caps configured or provider-reported values
|
|
205
201
|
at that limit. If an OpenAI-compatible model catalog reports a smaller
|
|
206
202
|
`context_window`, `context_length`, `max_context_length`, `max_model_len`, or
|
|
207
|
-
`n_ctx`, the smaller server value wins.
|
|
208
|
-
|
|
203
|
+
`n_ctx`, the smaller server value wins. The package-scoped provider's
|
|
204
|
+
`modelDefaults.contextWindow` can also select a smaller limit for dynamically
|
|
205
|
+
discovered models.
|
|
209
206
|
|
|
210
207
|
The default context policy is configured under `hwcode.context` in
|
|
211
208
|
`.pi/settings.json`:
|
package/bin/hwcode.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hadooppei/hwcode",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "A customizable terminal coding agent with local-model support and HWCode workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"pi": "node --env-file-if-exists=.env ./bin/hwcode.js",
|
|
27
27
|
"test": "node --test .pi/lib/*.test.ts",
|
|
28
28
|
"test:workflows": "npm test",
|
|
29
|
-
"prepack": "npm test"
|
|
29
|
+
"prepack": "npm test && node scripts/audit-package.mjs"
|
|
30
30
|
},
|
|
31
31
|
"engines": {
|
|
32
32
|
"node": ">=22.19.0"
|