@hadooppei/hwcode 0.1.0 → 0.2.1
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 +587 -0
- package/.pi/extensions/command-filter.ts +2 -6
- package/.pi/extensions/context-policy.ts +58 -0
- package/.pi/extensions/cwd.ts +12 -9
- package/.pi/extensions/model-providers.ts +92 -150
- package/.pi/extensions/welcome.ts +25 -9
- package/.pi/extensions/workflows.ts +41 -77
- package/.pi/lib/cloud/adapters.ts +234 -0
- package/.pi/lib/cloud/process.ts +91 -0
- package/.pi/lib/cloud/templates.ts +148 -0
- package/.pi/lib/cloud-providers.ts +263 -0
- package/.pi/lib/cloud-vault.ts +209 -0
- package/.pi/lib/command-filter.ts +1 -0
- package/.pi/lib/context/compaction.ts +113 -0
- package/.pi/lib/context-policy.ts +120 -0
- package/.pi/lib/models/provider-config.ts +68 -0
- package/.pi/lib/models/readiness.ts +18 -0
- package/.pi/lib/pixel-font.ts +5 -1
- package/.pi/lib/runtime/config.ts +70 -0
- package/.pi/lib/runtime/session-state.ts +61 -0
- package/.pi/lib/workflows/state.ts +138 -0
- package/.pi/lib/working-directory.ts +7 -7
- package/.pi/lib/workspace/access-policy.ts +49 -0
- package/.pi/model-providers.json +4 -44
- package/.pi/settings.json +41 -0
- package/.pi/skills/hwcode-cloud/SKILL.md +91 -0
- package/.pi/skills/hwcode-cloud/agents/openai.yaml +4 -0
- package/.pi/welcome.json +4 -2
- package/README.md +128 -36
- package/bin/hwcode.js +76 -4
- package/package.json +9 -8
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { loadLayeredJson } from "./runtime/config.ts";
|
|
2
|
+
|
|
3
|
+
export interface ContextPolicy {
|
|
4
|
+
defaultContextWindow: number;
|
|
5
|
+
maxContextWindow: number;
|
|
6
|
+
compactionTriggerTokens: number;
|
|
7
|
+
compactionTargetTokens: number;
|
|
8
|
+
compactionOverheadTokens: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CompactionBudgets {
|
|
12
|
+
triggerTokens: number;
|
|
13
|
+
targetTokens: number;
|
|
14
|
+
overheadTokens: number;
|
|
15
|
+
keepRecentTokens: number;
|
|
16
|
+
reserveTokens: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const DEFAULT_CONTEXT_POLICY: ContextPolicy = {
|
|
20
|
+
defaultContextWindow: 1_000_000,
|
|
21
|
+
maxContextWindow: 1_000_000,
|
|
22
|
+
compactionTriggerTokens: 920_000,
|
|
23
|
+
compactionTargetTokens: 300_000,
|
|
24
|
+
compactionOverheadTokens: 80_000,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
28
|
+
return typeof value === "object" && value !== null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function positiveInteger(value: unknown, name: string): number {
|
|
32
|
+
if (!Number.isSafeInteger(value) || (value as number) <= 0) {
|
|
33
|
+
throw new Error(`hwcode.context.${name} must be a positive integer`);
|
|
34
|
+
}
|
|
35
|
+
return value as number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function parseContextPolicy(
|
|
39
|
+
settingsText: string,
|
|
40
|
+
base: ContextPolicy = DEFAULT_CONTEXT_POLICY,
|
|
41
|
+
): ContextPolicy {
|
|
42
|
+
const settings = JSON.parse(settingsText) as unknown;
|
|
43
|
+
if (!isRecord(settings) || !isRecord(settings.hwcode) || settings.hwcode.context === undefined) {
|
|
44
|
+
return { ...base };
|
|
45
|
+
}
|
|
46
|
+
if (!isRecord(settings.hwcode.context)) {
|
|
47
|
+
throw new Error("hwcode.context must be an object");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const configured = settings.hwcode.context;
|
|
51
|
+
const policy: ContextPolicy = { ...base };
|
|
52
|
+
for (const key of Object.keys(policy) as Array<keyof ContextPolicy>) {
|
|
53
|
+
if (configured[key] !== undefined) {
|
|
54
|
+
policy[key] = positiveInteger(configured[key], key);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (policy.defaultContextWindow > policy.maxContextWindow) {
|
|
59
|
+
throw new Error("hwcode.context.defaultContextWindow cannot exceed maxContextWindow");
|
|
60
|
+
}
|
|
61
|
+
if (policy.compactionTargetTokens >= policy.compactionTriggerTokens) {
|
|
62
|
+
throw new Error("hwcode.context.compactionTargetTokens must be below compactionTriggerTokens");
|
|
63
|
+
}
|
|
64
|
+
if (policy.compactionTriggerTokens > policy.maxContextWindow) {
|
|
65
|
+
throw new Error("hwcode.context.compactionTriggerTokens cannot exceed maxContextWindow");
|
|
66
|
+
}
|
|
67
|
+
if (policy.compactionOverheadTokens >= policy.compactionTargetTokens) {
|
|
68
|
+
throw new Error("hwcode.context.compactionOverheadTokens must be below compactionTargetTokens");
|
|
69
|
+
}
|
|
70
|
+
return policy;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function loadContextPolicy(cwd: string): ContextPolicy {
|
|
74
|
+
const settings = loadLayeredJson(cwd, "settings.json", {
|
|
75
|
+
hwcode: { context: { ...DEFAULT_CONTEXT_POLICY } },
|
|
76
|
+
});
|
|
77
|
+
return parseContextPolicy(JSON.stringify(settings));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function resolveContextWindow(
|
|
81
|
+
configured: number | undefined,
|
|
82
|
+
reported: number | undefined,
|
|
83
|
+
policy: ContextPolicy,
|
|
84
|
+
): number {
|
|
85
|
+
const requested = configured ?? policy.defaultContextWindow;
|
|
86
|
+
return Math.min(requested, reported ?? Number.POSITIVE_INFINITY, policy.maxContextWindow);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function estimateTextTokens(text: string): number {
|
|
90
|
+
return Math.ceil(text.length / 4);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function calculateCompactionBudgets(
|
|
94
|
+
policy: ContextPolicy,
|
|
95
|
+
contextWindow: number,
|
|
96
|
+
systemPrompt = "",
|
|
97
|
+
): CompactionBudgets {
|
|
98
|
+
const triggerTokens = Math.min(
|
|
99
|
+
policy.compactionTriggerTokens,
|
|
100
|
+
Math.floor(contextWindow * 0.92),
|
|
101
|
+
);
|
|
102
|
+
const targetTokens = Math.min(
|
|
103
|
+
policy.compactionTargetTokens,
|
|
104
|
+
Math.floor(contextWindow * 0.6),
|
|
105
|
+
);
|
|
106
|
+
const scaledOverhead = Math.min(
|
|
107
|
+
policy.compactionOverheadTokens,
|
|
108
|
+
Math.floor(targetTokens * (policy.compactionOverheadTokens / policy.compactionTargetTokens)),
|
|
109
|
+
);
|
|
110
|
+
const overheadTokens = Math.max(scaledOverhead, estimateTextTokens(systemPrompt));
|
|
111
|
+
const keepRecentTokens = Math.max(1, targetTokens - overheadTokens);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
triggerTokens,
|
|
115
|
+
targetTokens,
|
|
116
|
+
overheadTokens,
|
|
117
|
+
keepRecentTokens,
|
|
118
|
+
reserveTokens: scaledOverhead,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
4
|
+
import { readJsonObject } from "../runtime/config.ts";
|
|
5
|
+
|
|
6
|
+
export type ModelInput = "text" | "image";
|
|
7
|
+
export interface ConfiguredModel { id: string; name?: string; reasoning?: boolean; input?: ModelInput[]; contextWindow?: number; maxTokens?: number; }
|
|
8
|
+
export interface ModelDefaults { reasoning?: boolean; input?: ModelInput[]; contextWindow?: number; maxTokens?: number; }
|
|
9
|
+
export interface LoginConfig { enabled: true; promptBaseUrl?: boolean; promptApiKey?: boolean; apiKeyRequired?: boolean; catalogPath?: string; timeoutMs?: number; }
|
|
10
|
+
export interface ModelProviderConfig {
|
|
11
|
+
id: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
baseUrl?: string;
|
|
14
|
+
baseUrlEnv?: string;
|
|
15
|
+
apiKeyEnv?: string;
|
|
16
|
+
login?: LoginConfig;
|
|
17
|
+
modelDefaults?: ModelDefaults;
|
|
18
|
+
models?: ConfiguredModel[];
|
|
19
|
+
}
|
|
20
|
+
export interface ModelProvidersConfig { providers: ModelProviderConfig[]; }
|
|
21
|
+
|
|
22
|
+
const BUNDLED_CONFIG_PATH = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "model-providers.json");
|
|
23
|
+
|
|
24
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
25
|
+
return typeof value === "object" && value !== null;
|
|
26
|
+
}
|
|
27
|
+
export function isPositiveNumber(value: unknown): value is number {
|
|
28
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
29
|
+
}
|
|
30
|
+
export function normalizeBaseUrl(value: string): string {
|
|
31
|
+
const url = new URL(value.trim());
|
|
32
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("Provider base URL must use http or https");
|
|
33
|
+
url.hash = "";
|
|
34
|
+
url.search = "";
|
|
35
|
+
return url.toString().replace(/\/$/u, "");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function validateModel(providerId: string, model: ConfiguredModel): void {
|
|
39
|
+
if (!model.id?.trim()) throw new Error(`Provider "${providerId}" contains a model without an id`);
|
|
40
|
+
if (model.input?.some((input) => input !== "text" && input !== "image")) throw new Error(`Model "${model.id}" has an unsupported input type`);
|
|
41
|
+
if (model.contextWindow !== undefined && !isPositiveNumber(model.contextWindow)) throw new Error(`Model "${model.id}" has an invalid contextWindow`);
|
|
42
|
+
if (model.maxTokens !== undefined && !isPositiveNumber(model.maxTokens)) throw new Error(`Model "${model.id}" has an invalid maxTokens`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function loadModelProvidersConfig(): ModelProvidersConfig {
|
|
46
|
+
const source = "model-providers.json";
|
|
47
|
+
const config = readJsonObject(BUNDLED_CONFIG_PATH) as ModelProvidersConfig & Record<string, unknown>;
|
|
48
|
+
if (!Array.isArray(config.providers) || config.providers.length === 0) throw new Error(`${source} must contain a non-empty providers array`);
|
|
49
|
+
const providerIds = new Set<string>();
|
|
50
|
+
for (const provider of config.providers) {
|
|
51
|
+
if (!provider.id?.trim()) throw new Error(`${source} contains a provider without an id`);
|
|
52
|
+
if (providerIds.has(provider.id)) throw new Error(`${source} contains duplicate provider id "${provider.id}"`);
|
|
53
|
+
providerIds.add(provider.id);
|
|
54
|
+
if (provider.baseUrl) normalizeBaseUrl(provider.baseUrl);
|
|
55
|
+
if (!provider.login?.enabled && !provider.baseUrl?.trim()) throw new Error(`Static provider "${provider.id}" requires baseUrl`);
|
|
56
|
+
if (!provider.login?.enabled && (!Array.isArray(provider.models) || provider.models.length === 0)) throw new Error(`Static provider "${provider.id}" must contain at least one model`);
|
|
57
|
+
if (provider.login?.enabled && provider.login.promptBaseUrl === false && !provider.baseUrl?.trim()) throw new Error(`Login provider "${provider.id}" requires baseUrl when promptBaseUrl is false`);
|
|
58
|
+
if (provider.login?.catalogPath !== undefined && !provider.login.catalogPath.trim()) throw new Error(`Login provider "${provider.id}" has an empty catalogPath`);
|
|
59
|
+
if (provider.login?.timeoutMs !== undefined && !isPositiveNumber(provider.login.timeoutMs)) throw new Error(`Login provider "${provider.id}" has an invalid timeoutMs`);
|
|
60
|
+
const modelIds = new Set<string>();
|
|
61
|
+
for (const model of provider.models ?? []) {
|
|
62
|
+
validateModel(provider.id, model);
|
|
63
|
+
if (modelIds.has(model.id)) throw new Error(`Provider "${provider.id}" contains duplicate model id "${model.id}"`);
|
|
64
|
+
modelIds.add(model.id);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return config;
|
|
68
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface ModelConfiguration {
|
|
2
|
+
provider: string;
|
|
3
|
+
id: string;
|
|
4
|
+
api: string;
|
|
5
|
+
baseUrl?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function modelConfigurationIssue(
|
|
9
|
+
model: ModelConfiguration | undefined,
|
|
10
|
+
providerEnvironment: Record<string, string | undefined> = {},
|
|
11
|
+
): string | undefined {
|
|
12
|
+
if (!model) return "当前会话没有可用模型";
|
|
13
|
+
if (model.api !== "azure-openai-responses") return undefined;
|
|
14
|
+
const baseUrl = model.baseUrl?.trim() || providerEnvironment.AZURE_OPENAI_BASE_URL?.trim();
|
|
15
|
+
const resourceName = providerEnvironment.AZURE_OPENAI_RESOURCE_NAME?.trim();
|
|
16
|
+
if (baseUrl || resourceName) return undefined;
|
|
17
|
+
return `当前模型 ${model.provider}/${model.id} 缺少 Azure OpenAI endpoint`;
|
|
18
|
+
}
|
package/.pi/lib/pixel-font.ts
CHANGED
|
@@ -230,6 +230,7 @@ export function renderPixelText(
|
|
|
230
230
|
theme: Theme,
|
|
231
231
|
availableWidth: number,
|
|
232
232
|
availableHeight: number,
|
|
233
|
+
scaleRatio = 1,
|
|
233
234
|
): PixelTextResult {
|
|
234
235
|
const letters = [...text.toUpperCase()];
|
|
235
236
|
const widthPerScale = letters.length * (GLYPH_WIDTH / BRAILLE_COLUMNS)
|
|
@@ -237,7 +238,10 @@ export function renderPixelText(
|
|
|
237
238
|
const heightPerScale = GLYPH_HEIGHT / BRAILLE_ROWS;
|
|
238
239
|
const widthScale = Math.floor((availableWidth - 4) / widthPerScale);
|
|
239
240
|
const heightScale = Math.floor(availableHeight / heightPerScale);
|
|
240
|
-
const
|
|
241
|
+
const naturalScale = Math.min(widthScale, heightScale);
|
|
242
|
+
const scale = naturalScale < 1
|
|
243
|
+
? naturalScale
|
|
244
|
+
: Math.max(1, Math.min(naturalScale, Math.floor(naturalScale * scaleRatio)));
|
|
241
245
|
|
|
242
246
|
if (scale < 1) {
|
|
243
247
|
const leftPadding = " ".repeat(Math.max(0, Math.floor((availableWidth - letters.length) / 2)));
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
export type JsonObject = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
export interface ConfigLocations {
|
|
7
|
+
profile?: string;
|
|
8
|
+
project: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isJsonObject(value: unknown): value is JsonObject {
|
|
12
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function deepMerge<T extends JsonObject>(base: T, override: JsonObject): T {
|
|
16
|
+
const merged: JsonObject = { ...base };
|
|
17
|
+
for (const [key, value] of Object.entries(override)) {
|
|
18
|
+
const current = merged[key];
|
|
19
|
+
merged[key] = isJsonObject(current) && isJsonObject(value)
|
|
20
|
+
? deepMerge(current, value)
|
|
21
|
+
: value;
|
|
22
|
+
}
|
|
23
|
+
return merged as T;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function profileResourcePath(fileName: string): string | undefined {
|
|
27
|
+
return process.env.HWCODE_PROFILE_DIR
|
|
28
|
+
? join(process.env.HWCODE_PROFILE_DIR, fileName)
|
|
29
|
+
: undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function configLocations(cwd: string, fileName: string): ConfigLocations {
|
|
33
|
+
return {
|
|
34
|
+
profile: profileResourcePath(fileName),
|
|
35
|
+
project: resolve(cwd, ".pi", fileName),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function readJsonObject(path: string): JsonObject {
|
|
40
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
41
|
+
if (!isJsonObject(parsed)) throw new Error(`${path} must contain a JSON object`);
|
|
42
|
+
return parsed;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function loadLayeredJson<T extends JsonObject>(
|
|
46
|
+
cwd: string,
|
|
47
|
+
fileName: string,
|
|
48
|
+
defaults: T,
|
|
49
|
+
): T {
|
|
50
|
+
const locations = configLocations(cwd, fileName);
|
|
51
|
+
let result = { ...defaults } as T;
|
|
52
|
+
for (const path of [locations.profile, locations.project]) {
|
|
53
|
+
if (path && existsSync(path)) result = deepMerge(result, readJsonObject(path));
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function loadReplacingResource<T extends JsonObject>(
|
|
59
|
+
cwd: string,
|
|
60
|
+
fileName: string,
|
|
61
|
+
fallbackPath: string,
|
|
62
|
+
): T {
|
|
63
|
+
const locations = configLocations(cwd, fileName);
|
|
64
|
+
const path = existsSync(locations.project)
|
|
65
|
+
? locations.project
|
|
66
|
+
: locations.profile && existsSync(locations.profile)
|
|
67
|
+
? locations.profile
|
|
68
|
+
: fallbackPath;
|
|
69
|
+
return readJsonObject(path) as T;
|
|
70
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export interface CustomSessionEntry {
|
|
2
|
+
type: string;
|
|
3
|
+
customType?: string;
|
|
4
|
+
data?: unknown;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface SessionEntrySource {
|
|
8
|
+
getEntries(): readonly CustomSessionEntry[];
|
|
9
|
+
getSessionId(): string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface StateCodec<T> {
|
|
13
|
+
customType: string;
|
|
14
|
+
decode(value: unknown): T | undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface StateLookup<T> {
|
|
18
|
+
found: boolean;
|
|
19
|
+
value?: T;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function findLatestState<T>(
|
|
23
|
+
entries: readonly CustomSessionEntry[],
|
|
24
|
+
codec: StateCodec<T>,
|
|
25
|
+
): StateLookup<T> {
|
|
26
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
27
|
+
const entry = entries[index];
|
|
28
|
+
if (entry.type !== "custom" || entry.customType !== codec.customType) continue;
|
|
29
|
+
return { found: true, value: codec.decode(entry.data) };
|
|
30
|
+
}
|
|
31
|
+
return { found: false };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class SessionStateCache<T> {
|
|
35
|
+
private readonly values = new Map<string, T | undefined>();
|
|
36
|
+
private readonly codec: StateCodec<T>;
|
|
37
|
+
|
|
38
|
+
constructor(codec: StateCodec<T>) {
|
|
39
|
+
this.codec = codec;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
restore(source: SessionEntrySource): T | undefined {
|
|
43
|
+
const lookup = findLatestState(source.getEntries(), this.codec);
|
|
44
|
+
const value = lookup.value;
|
|
45
|
+
this.values.set(source.getSessionId(), value);
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get(source: SessionEntrySource): T | undefined {
|
|
50
|
+
if (!this.values.has(source.getSessionId())) return this.restore(source);
|
|
51
|
+
return this.values.get(source.getSessionId());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
set(source: SessionEntrySource, value: T | undefined): void {
|
|
55
|
+
this.values.set(source.getSessionId(), value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
clear(sessionId: string): void {
|
|
59
|
+
this.values.delete(sessionId);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { findLatestState, type CustomSessionEntry, type StateCodec } from "../runtime/session-state.ts";
|
|
2
|
+
|
|
3
|
+
export const WORKFLOW_STATE_TYPE = "hwcode-workflow-state";
|
|
4
|
+
export const WORKFLOW_EXTERNAL_AUDIT_TYPE = "hwcode-workflow-external-approval";
|
|
5
|
+
|
|
6
|
+
export type WorkflowMode = "vibe" | "sdd" | "cloud";
|
|
7
|
+
export type WorkflowStatus = "active" | "completed" | "cancelled" | "failed";
|
|
8
|
+
|
|
9
|
+
export interface FailedApproach {
|
|
10
|
+
approach: string;
|
|
11
|
+
reason: string;
|
|
12
|
+
failedAt: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CloudExecutionStep {
|
|
16
|
+
command: string;
|
|
17
|
+
args: string[];
|
|
18
|
+
operation: "read" | "change" | "delete";
|
|
19
|
+
intent: string;
|
|
20
|
+
approach: string;
|
|
21
|
+
completedAt: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CloudWorkflowDetails {
|
|
25
|
+
vendor: "aws" | "azure" | "gcp" | "huawei" | "alibaba" | "tencent";
|
|
26
|
+
deployCurrentProject: boolean;
|
|
27
|
+
request: string;
|
|
28
|
+
allowNonDeleteChanges: boolean;
|
|
29
|
+
failedApproaches: FailedApproach[];
|
|
30
|
+
successfulSteps: CloudExecutionStep[];
|
|
31
|
+
terminalFailure: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface WorkflowState {
|
|
35
|
+
version: 2;
|
|
36
|
+
status: WorkflowStatus;
|
|
37
|
+
mode: WorkflowMode;
|
|
38
|
+
root: string;
|
|
39
|
+
phase: string;
|
|
40
|
+
activatedAt: string;
|
|
41
|
+
updatedAt: string;
|
|
42
|
+
details?: CloudWorkflowDetails;
|
|
43
|
+
reason?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface LegacyWorkflowState {
|
|
47
|
+
version: 1;
|
|
48
|
+
active: boolean;
|
|
49
|
+
mode?: WorkflowMode;
|
|
50
|
+
root?: string;
|
|
51
|
+
activatedAt?: string;
|
|
52
|
+
deactivatedAt?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isCloudDetails(value: unknown): value is CloudWorkflowDetails {
|
|
56
|
+
if (!value || typeof value !== "object") return false;
|
|
57
|
+
const data = value as Record<string, unknown>;
|
|
58
|
+
return typeof data.vendor === "string"
|
|
59
|
+
&& typeof data.deployCurrentProject === "boolean"
|
|
60
|
+
&& typeof data.request === "string"
|
|
61
|
+
&& typeof data.allowNonDeleteChanges === "boolean"
|
|
62
|
+
&& Array.isArray(data.failedApproaches)
|
|
63
|
+
&& Array.isArray(data.successfulSteps)
|
|
64
|
+
&& typeof data.terminalFailure === "boolean";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function decodeWorkflowState(value: unknown): WorkflowState | undefined {
|
|
68
|
+
if (!value || typeof value !== "object") return undefined;
|
|
69
|
+
const data = value as Record<string, unknown>;
|
|
70
|
+
if (data.version === 2) {
|
|
71
|
+
if (!(["active", "completed", "cancelled", "failed"] as unknown[]).includes(data.status)
|
|
72
|
+
|| !(["vibe", "sdd", "cloud"] as unknown[]).includes(data.mode)
|
|
73
|
+
|| typeof data.root !== "string"
|
|
74
|
+
|| typeof data.phase !== "string"
|
|
75
|
+
|| typeof data.activatedAt !== "string"
|
|
76
|
+
|| typeof data.updatedAt !== "string") return undefined;
|
|
77
|
+
if (data.mode === "cloud" && !isCloudDetails(data.details)) return undefined;
|
|
78
|
+
return data as unknown as WorkflowState;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const legacy = data as unknown as LegacyWorkflowState;
|
|
82
|
+
if (legacy.version !== 1 || !legacy.active || !legacy.mode || !legacy.root || !legacy.activatedAt) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
version: 2,
|
|
87
|
+
status: "active",
|
|
88
|
+
mode: legacy.mode,
|
|
89
|
+
root: legacy.root,
|
|
90
|
+
phase: "legacy",
|
|
91
|
+
activatedAt: legacy.activatedAt,
|
|
92
|
+
updatedAt: legacy.activatedAt,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const WORKFLOW_STATE_CODEC: StateCodec<WorkflowState> = {
|
|
97
|
+
customType: WORKFLOW_STATE_TYPE,
|
|
98
|
+
decode: decodeWorkflowState,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export function restoreWorkflowState(entries: readonly CustomSessionEntry[]): WorkflowState | undefined {
|
|
102
|
+
return findLatestState(entries, WORKFLOW_STATE_CODEC).value;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function activeWorkflow(entries: readonly CustomSessionEntry[]): WorkflowState | undefined {
|
|
106
|
+
const state = restoreWorkflowState(entries);
|
|
107
|
+
return state?.status === "active" ? state : undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function createWorkflowState(
|
|
111
|
+
mode: "vibe" | "sdd",
|
|
112
|
+
root: string,
|
|
113
|
+
phase = "activated",
|
|
114
|
+
): WorkflowState {
|
|
115
|
+
const now = new Date().toISOString();
|
|
116
|
+
return {
|
|
117
|
+
version: 2,
|
|
118
|
+
status: "active",
|
|
119
|
+
mode,
|
|
120
|
+
root,
|
|
121
|
+
phase,
|
|
122
|
+
activatedAt: now,
|
|
123
|
+
updatedAt: now,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function updateWorkflowState(
|
|
128
|
+
state: WorkflowState,
|
|
129
|
+
changes: Partial<Omit<WorkflowState, "version" | "mode" | "root" | "activatedAt">>,
|
|
130
|
+
): WorkflowState {
|
|
131
|
+
return { ...state, ...changes, version: 2, updatedAt: new Date().toISOString() };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function workflowLabel(mode: WorkflowMode): string {
|
|
135
|
+
if (mode === "vibe") return "HWCode Vibe";
|
|
136
|
+
if (mode === "sdd") return "HWCode SDD";
|
|
137
|
+
return "HWCode Cloud";
|
|
138
|
+
}
|
|
@@ -2,6 +2,8 @@ import { realpathSync, statSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { isAbsolute, resolve } from "node:path";
|
|
4
4
|
|
|
5
|
+
import { activeWorkflow } from "./workflows/state.ts";
|
|
6
|
+
|
|
5
7
|
export const WORKING_DIRECTORY_STATE_TYPE = "hwcode-working-directory";
|
|
6
8
|
|
|
7
9
|
interface SessionEntry {
|
|
@@ -85,6 +87,10 @@ export function resetWorkingDirectoryState(source: SessionDirectorySource): Work
|
|
|
85
87
|
return state;
|
|
86
88
|
}
|
|
87
89
|
|
|
90
|
+
export function clearWorkingDirectoryState(source: Pick<SessionDirectorySource, "getSessionId">): void {
|
|
91
|
+
workingDirectories.delete(source.getSessionId());
|
|
92
|
+
}
|
|
93
|
+
|
|
88
94
|
function expandHome(path: string, home: string): string {
|
|
89
95
|
if (path === "~" || path === "$HOME" || path === "${HOME}") return home;
|
|
90
96
|
if (path.startsWith("~/")) return resolve(home, path.slice(2));
|
|
@@ -238,13 +244,7 @@ export function findChainedDirectoryChange(command: string): ChainedDirectoryCha
|
|
|
238
244
|
}
|
|
239
245
|
|
|
240
246
|
export function getActiveWorkflowRoot(entries: readonly SessionEntry[]): string | undefined {
|
|
241
|
-
|
|
242
|
-
if (entry.type !== "custom" || entry.customType !== "hwcode-workflow-state") continue;
|
|
243
|
-
if (!entry.data || typeof entry.data !== "object") return undefined;
|
|
244
|
-
const data = entry.data as Record<string, unknown>;
|
|
245
|
-
return data.active === true && typeof data.root === "string" ? data.root : undefined;
|
|
246
|
-
}
|
|
247
|
-
return undefined;
|
|
247
|
+
return activeWorkflow(entries)?.root;
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
export function shellQuote(value: string): string {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { homedir, tmpdir } from "node:os";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
findExternalPathReferences,
|
|
5
|
+
isPathInsideRoot,
|
|
6
|
+
resolveToolPath,
|
|
7
|
+
type ExternalPathReference,
|
|
8
|
+
} from "../workflow-guard.ts";
|
|
9
|
+
|
|
10
|
+
export const FILE_PATH_TOOLS = new Set(["read", "write", "edit", "grep", "find", "ls"]);
|
|
11
|
+
export const OPTIONAL_PATH_TOOLS = new Set(["grep", "find", "ls"]);
|
|
12
|
+
|
|
13
|
+
export interface ToolAccessRequest {
|
|
14
|
+
toolName: string;
|
|
15
|
+
input: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function evaluateToolPathAccess(
|
|
19
|
+
request: ToolAccessRequest,
|
|
20
|
+
root: string,
|
|
21
|
+
): ExternalPathReference[] {
|
|
22
|
+
if (FILE_PATH_TOOLS.has(request.toolName) && typeof request.input.path === "string") {
|
|
23
|
+
const resolved = resolveToolPath(root, request.input.path, homedir());
|
|
24
|
+
return isPathInsideRoot(root, resolved)
|
|
25
|
+
? []
|
|
26
|
+
: [{ raw: request.input.path, resolved }];
|
|
27
|
+
}
|
|
28
|
+
if (request.toolName === "bash" && typeof request.input.command === "string") {
|
|
29
|
+
return findExternalPathReferences(request.input.command, root, {
|
|
30
|
+
home: homedir(),
|
|
31
|
+
tmpdir: tmpdir(),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function evaluateCommandArgumentsAccess(
|
|
38
|
+
command: string,
|
|
39
|
+
args: readonly string[],
|
|
40
|
+
root: string,
|
|
41
|
+
): ExternalPathReference[] {
|
|
42
|
+
const syntheticCommand = [command, ...args]
|
|
43
|
+
.map((value) => `'${value.replaceAll("'", `'"'"'`)}'`)
|
|
44
|
+
.join(" ");
|
|
45
|
+
return findExternalPathReferences(syntheticCommand, root, {
|
|
46
|
+
home: homedir(),
|
|
47
|
+
tmpdir: tmpdir(),
|
|
48
|
+
});
|
|
49
|
+
}
|
package/.pi/model-providers.json
CHANGED
|
@@ -1,62 +1,22 @@
|
|
|
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
|
-
"models": [
|
|
9
|
-
{
|
|
10
|
-
"id": "qwen3-VL:2b",
|
|
11
|
-
"name": "Qwen3 VL 2B",
|
|
12
|
-
"input": ["text", "image"],
|
|
13
|
-
"contextWindow": 32768,
|
|
14
|
-
"maxTokens": 8192
|
|
15
|
-
}
|
|
16
|
-
]
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
"id": "local-8080",
|
|
20
|
-
"name": "Qwen3.5 9B (8080)",
|
|
21
|
-
"baseUrl": "http://127.0.0.1:8080",
|
|
22
|
-
"apiKeyEnv": "PI_LOCAL_MODEL_API_KEY",
|
|
23
|
-
"models": [
|
|
24
|
-
{
|
|
25
|
-
"id": "Qwen3.5-9B-Q4_K_M",
|
|
26
|
-
"name": "Qwen3.5 9B Q4_K_M",
|
|
27
|
-
"input": ["text"],
|
|
28
|
-
"contextWindow": 32768,
|
|
29
|
-
"maxTokens": 8192
|
|
30
|
-
}
|
|
31
|
-
]
|
|
32
|
-
},
|
|
33
3
|
{
|
|
34
4
|
"id": "hw",
|
|
35
|
-
"name": "
|
|
36
|
-
"baseUrl": "http://127.0.0.1:8080/v1",
|
|
5
|
+
"name": "Huawei MaaS",
|
|
37
6
|
"apiKeyEnv": "HW_API_KEY",
|
|
38
7
|
"login": {
|
|
39
8
|
"enabled": true,
|
|
40
9
|
"promptBaseUrl": true,
|
|
41
10
|
"promptApiKey": true,
|
|
42
|
-
"apiKeyRequired":
|
|
11
|
+
"apiKeyRequired": true,
|
|
43
12
|
"catalogPath": "models",
|
|
44
13
|
"timeoutMs": 15000
|
|
45
14
|
},
|
|
46
15
|
"modelDefaults": {
|
|
47
16
|
"input": ["text"],
|
|
48
|
-
"contextWindow":
|
|
17
|
+
"contextWindow": 1000000,
|
|
49
18
|
"maxTokens": 8192
|
|
50
|
-
}
|
|
51
|
-
"models": [
|
|
52
|
-
{
|
|
53
|
-
"id": "Qwen3.5-9B-Q4_K_M",
|
|
54
|
-
"name": "Qwen3.5 9B Q4_K_M",
|
|
55
|
-
"input": ["text"],
|
|
56
|
-
"contextWindow": 32768,
|
|
57
|
-
"maxTokens": 8192
|
|
58
|
-
}
|
|
59
|
-
]
|
|
19
|
+
}
|
|
60
20
|
}
|
|
61
21
|
]
|
|
62
22
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"defaultThinkingLevel": "medium",
|
|
3
|
+
"quietStartup": true,
|
|
4
|
+
"tuiMode": "fullscreen",
|
|
5
|
+
"sessionDir": ".pi/sessions",
|
|
6
|
+
"retry": {
|
|
7
|
+
"enabled": true,
|
|
8
|
+
"maxRetries": 3
|
|
9
|
+
},
|
|
10
|
+
"compaction": {
|
|
11
|
+
"enabled": true,
|
|
12
|
+
"reserveTokens": 80000,
|
|
13
|
+
"keepRecentTokens": 220000
|
|
14
|
+
},
|
|
15
|
+
"hwcode": {
|
|
16
|
+
"context": {
|
|
17
|
+
"defaultContextWindow": 1000000,
|
|
18
|
+
"maxContextWindow": 1000000,
|
|
19
|
+
"compactionTriggerTokens": 920000,
|
|
20
|
+
"compactionTargetTokens": 300000,
|
|
21
|
+
"compactionOverheadTokens": 80000
|
|
22
|
+
},
|
|
23
|
+
"hiddenCommands": [
|
|
24
|
+
"export",
|
|
25
|
+
"import",
|
|
26
|
+
"share",
|
|
27
|
+
"name",
|
|
28
|
+
"changelog",
|
|
29
|
+
"fork",
|
|
30
|
+
"clone",
|
|
31
|
+
"trust",
|
|
32
|
+
"reload",
|
|
33
|
+
"review",
|
|
34
|
+
"welcome",
|
|
35
|
+
"llama",
|
|
36
|
+
"skill:hwcode-vibe",
|
|
37
|
+
"skill:hwcode-sdd",
|
|
38
|
+
"skill:hwcode-cloud"
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
}
|