@hadooppei/hwcode 0.1.0 → 0.2.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.
@@ -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 { loadReplacingResource } 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(cwd = process.cwd()): ModelProvidersConfig {
46
+ const source = "model-providers.json";
47
+ const config = loadReplacingResource<ModelProvidersConfig & Record<string, unknown>>(cwd, source, BUNDLED_CONFIG_PATH);
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
+ }
@@ -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 scale = Math.min(widthScale, heightScale);
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
- for (const entry of [...entries].reverse()) {
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
+ }
@@ -5,12 +5,16 @@
5
5
  "name": "Qwen3 VL (8081)",
6
6
  "baseUrl": "http://127.0.0.1:8081",
7
7
  "apiKeyEnv": "PI_LOCAL_MODEL_API_KEY",
8
+ "modelDefaults": {
9
+ "contextWindow": 1000000,
10
+ "maxTokens": 8192
11
+ },
8
12
  "models": [
9
13
  {
10
14
  "id": "qwen3-VL:2b",
11
15
  "name": "Qwen3 VL 2B",
12
16
  "input": ["text", "image"],
13
- "contextWindow": 32768,
17
+ "contextWindow": 1000000,
14
18
  "maxTokens": 8192
15
19
  }
16
20
  ]
@@ -20,19 +24,23 @@
20
24
  "name": "Qwen3.5 9B (8080)",
21
25
  "baseUrl": "http://127.0.0.1:8080",
22
26
  "apiKeyEnv": "PI_LOCAL_MODEL_API_KEY",
27
+ "modelDefaults": {
28
+ "contextWindow": 1000000,
29
+ "maxTokens": 8192
30
+ },
23
31
  "models": [
24
32
  {
25
33
  "id": "Qwen3.5-9B-Q4_K_M",
26
34
  "name": "Qwen3.5 9B Q4_K_M",
27
35
  "input": ["text"],
28
- "contextWindow": 32768,
36
+ "contextWindow": 1000000,
29
37
  "maxTokens": 8192
30
38
  }
31
39
  ]
32
40
  },
33
41
  {
34
42
  "id": "hw",
35
- "name": "hw",
43
+ "name": "Huawei MaaS",
36
44
  "baseUrl": "http://127.0.0.1:8080/v1",
37
45
  "apiKeyEnv": "HW_API_KEY",
38
46
  "login": {
@@ -45,7 +53,7 @@
45
53
  },
46
54
  "modelDefaults": {
47
55
  "input": ["text"],
48
- "contextWindow": 32768,
56
+ "contextWindow": 1000000,
49
57
  "maxTokens": 8192
50
58
  },
51
59
  "models": [
@@ -53,7 +61,7 @@
53
61
  "id": "Qwen3.5-9B-Q4_K_M",
54
62
  "name": "Qwen3.5 9B Q4_K_M",
55
63
  "input": ["text"],
56
- "contextWindow": 32768,
64
+ "contextWindow": 1000000,
57
65
  "maxTokens": 8192
58
66
  }
59
67
  ]
@@ -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
+ }