pi-chalin 0.1.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,140 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { sessionModelOverrides, sessionThinkingOverrides } from "./agent-overrides.ts";
3
+ import { AgentCatalog } from "./agents.ts";
4
+ import { ArtifactStore } from "./artifacts.ts";
5
+ import { loadEffectiveConfig, writeProjectConfig } from "./config.ts";
6
+ import { MemoryStore } from "./memory.ts";
7
+ import { getActiveRun, getLatestRun } from "./runtime-state.ts";
8
+ import { openAgentManager } from "./ui-agents.ts";
9
+ import {
10
+ openMemoryReview,
11
+ openArtifactPanel,
12
+ openActivityMonitor,
13
+ openWebFetchAuditPanel,
14
+ openSmartPanel,
15
+ } from "./ui.ts";
16
+ import { setChalinStatus } from "./ui-status.ts";
17
+ import { listWebFetchAudit } from "./webfetch.ts";
18
+
19
+ export function registerChalinCommands(pi: ExtensionAPI): void {
20
+ pi.registerCommand("chalin", {
21
+ description: "Open pi-chalin Smart Panel or toggle autonomous routing with: /chalin on|off",
22
+ getArgumentCompletions: (prefix) => {
23
+ const values = ["on", "off", "agents", "memory", "artifacts", "activity", "web", "status"];
24
+ const filtered = values.filter((value) => value.startsWith(prefix.trim()));
25
+ return filtered.length > 0 ? filtered.map((value) => ({ value, label: value })) : null;
26
+ },
27
+ handler: async (args, ctx) => {
28
+ const commandLine = args.trim();
29
+ const [command = "", ...rest] = commandLine.split(/\s+/);
30
+
31
+ if (command === "on" || command === "off") {
32
+ const enabled = command === "on";
33
+ const loaded = writeProjectConfig({ cwd: ctx.cwd }, { enabled });
34
+ setChalinStatus(ctx, { kind: enabled ? "on" : "off" });
35
+ ctx.ui.notify(`pi-chalin autonomous routing ${enabled ? "enabled" : "disabled"} for this project.`, "info");
36
+ if (loaded.diagnostics.length > 0) ctx.ui.notify(loaded.diagnostics.join("\n"), "warning");
37
+ return;
38
+ }
39
+
40
+ const loaded = loadEffectiveConfig({ cwd: ctx.cwd });
41
+ const catalog = AgentCatalog.load({ cwd: ctx.cwd });
42
+ const memory = new MemoryStore({ cwd: ctx.cwd });
43
+ const artifacts = new ArtifactStore({ cwd: ctx.cwd });
44
+ const agents = catalog.list();
45
+ const diagnostics = [...loaded.diagnostics, ...catalog.diagnostics.warnings, ...catalog.diagnostics.errors];
46
+ const pendingMemories = await memory.list("pending");
47
+ const activeRun = getActiveRun();
48
+ const lastRun = getLatestRun();
49
+
50
+ if (command === "agents") {
51
+ await openAgentManager(ctx, agents, sessionModelOverrides, sessionThinkingOverrides, loaded.config.agents.modelOverrides, loaded.config.agents.thinkingOverrides);
52
+ return;
53
+ }
54
+
55
+ if (command === "memory") {
56
+ const query = rest.join(" ").trim();
57
+ if (query) {
58
+ const bundle = await memory.retrieve({ query, sourceAgent: "human-command", limit: 10, tokenBudget: 1200, includeEvidence: true });
59
+ ctx.ui.notify(bundle.text || "No memory matches.", "info");
60
+ } else {
61
+ await openMemoryReview(ctx, await memory.list(), {
62
+ approve: (id) => void memory.approve(id),
63
+ reject: (id) => void memory.reject(id),
64
+ delete: (id) => void memory.delete(id),
65
+ });
66
+ }
67
+ return;
68
+ }
69
+
70
+
71
+ if (command === "artifacts") {
72
+ const featureId = rest.join(" ").trim();
73
+ if (featureId) {
74
+ ctx.ui.notify(await artifacts.resumeContext(featureId), "info");
75
+ return;
76
+ }
77
+ await openArtifactPanel(ctx, artifacts);
78
+ return;
79
+ }
80
+
81
+ if (command === "activity" || command === "runs") {
82
+ await openActivityMonitor(ctx, activeRun ?? lastRun);
83
+ return;
84
+ }
85
+
86
+ if (command === "web" || command === "webfetch") {
87
+ await openWebFetchAuditPanel(ctx, await listWebFetchAudit({ cwd: ctx.cwd }));
88
+ return;
89
+ }
90
+
91
+ if (command === "status") {
92
+ ctx.ui.notify(
93
+ [
94
+ `routing: ${loaded.config.enabled ? "on" : "off"}`,
95
+ `autonomy: ${loaded.config.autonomy}`,
96
+ `approval threshold: ${loaded.config.safety.approvalRiskThreshold}`,
97
+ `agents: ${agents.length}`,
98
+ `pending memory: ${pendingMemories.length}`,
99
+ `last activity: ${lastRun?.id ?? "none"}`,
100
+ lastRun ? `guards: ${lastRun.metrics?.policyViolations?.length ?? 0} policy violations · ${lastRun.metrics?.budgetStopCount ?? 0} budget stops` : "guards: no run yet",
101
+ ].join("\n"),
102
+ "info",
103
+ );
104
+ return;
105
+ }
106
+
107
+ if (command && command !== "panel") {
108
+ ctx.ui.notify(`Unknown /chalin argument '${command}'. Use /chalin, /chalin activity, /chalin artifacts, /chalin web, /chalin on, or /chalin off. Normal prompts are routed automatically when enabled.`, "warning");
109
+ return;
110
+ }
111
+
112
+ if (activeRun) {
113
+ await openActivityMonitor(ctx, activeRun);
114
+ return;
115
+ }
116
+
117
+ await openSmartPanel(ctx, {
118
+ state: {
119
+ autoRoutingEnabled: loaded.config.enabled,
120
+ pendingApprovals: 0,
121
+ activeRuns: activeRun ? 1 : 0,
122
+ pendingMemoryCandidates: pendingMemories.length,
123
+ lastRun,
124
+ },
125
+ agents,
126
+ diagnostics,
127
+ pendingMemories,
128
+ onSelectAgents: () => openAgentManager(ctx, agents, sessionModelOverrides, sessionThinkingOverrides, loaded.config.agents.modelOverrides, loaded.config.agents.thinkingOverrides),
129
+ onSelectActivity: () => openActivityMonitor(ctx, activeRun ?? lastRun),
130
+ onSelectMemory: async () => openMemoryReview(ctx, await memory.list(), {
131
+ approve: (id) => void memory.approve(id),
132
+ reject: (id) => void memory.reject(id),
133
+ delete: (id) => void memory.delete(id),
134
+ }),
135
+ onSelectArtifacts: () => openArtifactPanel(ctx, artifacts),
136
+ onSelectWebFetch: async () => openWebFetchAuditPanel(ctx, await listWebFetchAudit({ cwd: ctx.cwd })),
137
+ });
138
+ },
139
+ });
140
+ }
package/src/config.ts ADDED
@@ -0,0 +1,189 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { resolveChalinPaths, type ChalinPathsOptions } from "./paths.ts";
4
+ import { isAgentThinkingLevel, riskRank, type AgentScope, type AgentThinkingLevel, type ApprovalDecision, type RouteDecision, type RouteRisk } from "./schemas.ts";
5
+
6
+ export type AutonomyLevel = "low" | "balanced" | "high";
7
+ export type ApprovalRiskThreshold = RouteRisk;
8
+ export type ModelPersistenceTarget = "session" | "project" | "user";
9
+
10
+ export interface ChalinConfig {
11
+ enabled: boolean;
12
+ autonomy: AutonomyLevel;
13
+ safety: {
14
+ approvalRiskThreshold: ApprovalRiskThreshold;
15
+ recursionGuard: boolean;
16
+ singleWriterGuard: boolean;
17
+ mutationExpectationGuard: boolean;
18
+ blockCritical: boolean;
19
+ };
20
+ agents: {
21
+ modelOverrides: Record<string, string>;
22
+ thinkingOverrides: Record<string, AgentThinkingLevel>;
23
+ modelPersistenceDefaults: Record<AgentScope, ModelPersistenceTarget>;
24
+ };
25
+ }
26
+
27
+ export interface LoadedChalinConfig {
28
+ config: ChalinConfig;
29
+ diagnostics: string[];
30
+ paths: ReturnType<typeof resolveChalinPaths>;
31
+ }
32
+
33
+ export const DEFAULT_CONFIG: ChalinConfig = {
34
+ enabled: true,
35
+ autonomy: "balanced",
36
+ safety: {
37
+ approvalRiskThreshold: "medium",
38
+ recursionGuard: true,
39
+ singleWriterGuard: true,
40
+ mutationExpectationGuard: true,
41
+ blockCritical: true,
42
+ },
43
+ agents: {
44
+ modelOverrides: {},
45
+ thinkingOverrides: {},
46
+ modelPersistenceDefaults: {
47
+ "built-in": "user",
48
+ user: "user",
49
+ project: "project",
50
+ },
51
+ },
52
+ };
53
+
54
+ function isObject(value: unknown): value is Record<string, unknown> {
55
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
56
+ }
57
+
58
+ function deepMerge<T>(base: T, override: unknown): T {
59
+ if (!isObject(base) || !isObject(override)) return override === undefined ? base : (override as T);
60
+ const result: Record<string, unknown> = { ...base };
61
+ for (const [key, value] of Object.entries(override)) {
62
+ const current = result[key];
63
+ result[key] = isObject(current) && isObject(value) ? deepMerge(current, value) : value;
64
+ }
65
+ return result as T;
66
+ }
67
+
68
+ function readJsonObject(filePath: string, diagnostics: string[]): Record<string, unknown> {
69
+ if (!fs.existsSync(filePath)) return {};
70
+ try {
71
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8")) as unknown;
72
+ if (!isObject(parsed)) {
73
+ diagnostics.push(`Config '${filePath}' must contain a JSON object.`);
74
+ return {};
75
+ }
76
+ return parsed;
77
+ } catch (error) {
78
+ const message = error instanceof Error ? error.message : String(error);
79
+ diagnostics.push(`Failed to read config '${filePath}': ${message}`);
80
+ return {};
81
+ }
82
+ }
83
+
84
+ function coerceConfig(input: ChalinConfig, diagnostics: string[]): ChalinConfig {
85
+ const config = deepMerge(DEFAULT_CONFIG, input);
86
+
87
+ if (!["low", "balanced", "high"].includes(config.autonomy)) {
88
+ diagnostics.push(`Invalid autonomy '${String(config.autonomy)}'; using '${DEFAULT_CONFIG.autonomy}'.`);
89
+ config.autonomy = DEFAULT_CONFIG.autonomy;
90
+ }
91
+
92
+ if (!["low", "medium", "high", "critical"].includes(config.safety.approvalRiskThreshold)) {
93
+ diagnostics.push(
94
+ `Invalid safety.approvalRiskThreshold '${String(config.safety.approvalRiskThreshold)}'; using '${DEFAULT_CONFIG.safety.approvalRiskThreshold}'.`,
95
+ );
96
+ config.safety.approvalRiskThreshold = DEFAULT_CONFIG.safety.approvalRiskThreshold;
97
+ }
98
+
99
+ if (riskRank(config.safety.approvalRiskThreshold) > riskRank(DEFAULT_CONFIG.safety.approvalRiskThreshold)) {
100
+ diagnostics.push(
101
+ `Safety non-downgrade enforced: approvalRiskThreshold '${config.safety.approvalRiskThreshold}' is weaker than '${DEFAULT_CONFIG.safety.approvalRiskThreshold}'.`,
102
+ );
103
+ config.safety.approvalRiskThreshold = DEFAULT_CONFIG.safety.approvalRiskThreshold;
104
+ }
105
+
106
+ for (const key of ["recursionGuard", "singleWriterGuard", "mutationExpectationGuard", "blockCritical"] as const) {
107
+ if (DEFAULT_CONFIG.safety[key] && config.safety[key] !== true) {
108
+ diagnostics.push(`Safety non-downgrade enforced: safety.${key} cannot be disabled.`);
109
+ config.safety[key] = true;
110
+ }
111
+ }
112
+
113
+ if (!isObject(config.agents.modelOverrides)) config.agents.modelOverrides = {};
114
+ if (!isObject(config.agents.thinkingOverrides)) config.agents.thinkingOverrides = {};
115
+ for (const [agentRef, level] of Object.entries(config.agents.thinkingOverrides)) {
116
+ if (typeof level !== "string" || !isAgentThinkingLevel(level)) {
117
+ diagnostics.push(`Invalid agents.thinkingOverrides['${agentRef}']='${String(level)}'; removing override.`);
118
+ delete config.agents.thinkingOverrides[agentRef];
119
+ }
120
+ }
121
+ return config;
122
+ }
123
+
124
+ export function loadEffectiveConfig(options: ChalinPathsOptions): LoadedChalinConfig {
125
+ const paths = resolveChalinPaths(options);
126
+ const diagnostics: string[] = [];
127
+ const projectConfig = readJsonObject(paths.projectConfigPath, diagnostics);
128
+ const userConfig = readJsonObject(paths.userConfigPath, diagnostics);
129
+ const merged = deepMerge(deepMerge(DEFAULT_CONFIG, projectConfig), userConfig);
130
+ return { config: coerceConfig(merged, diagnostics), diagnostics, paths };
131
+ }
132
+
133
+ export function writeProjectConfig(options: ChalinPathsOptions, configPatch: Partial<ChalinConfig>): LoadedChalinConfig {
134
+ const loaded = loadEffectiveConfig(options);
135
+ const current = readJsonObject(loaded.paths.projectConfigPath, []);
136
+ const next = deepMerge(current, configPatch);
137
+ fs.mkdirSync(path.dirname(loaded.paths.projectConfigPath), { recursive: true });
138
+ fs.writeFileSync(loaded.paths.projectConfigPath, `${JSON.stringify(next, null, 2)}\n`, "utf-8");
139
+ return loadEffectiveConfig(options);
140
+ }
141
+
142
+ export function writeUserConfig(options: ChalinPathsOptions, configPatch: Partial<ChalinConfig>): LoadedChalinConfig {
143
+ const loaded = loadEffectiveConfig(options);
144
+ const current = readJsonObject(loaded.paths.userConfigPath, []);
145
+ const next = deepMerge(current, configPatch);
146
+ fs.mkdirSync(path.dirname(loaded.paths.userConfigPath), { recursive: true });
147
+ fs.writeFileSync(loaded.paths.userConfigPath, `${JSON.stringify(next, null, 2)}\n`, "utf-8");
148
+ return loadEffectiveConfig(options);
149
+ }
150
+
151
+ export function setAgentModelOverride(
152
+ options: ChalinPathsOptions,
153
+ agentRef: string,
154
+ model: string | undefined,
155
+ target: Exclude<ModelPersistenceTarget, "session">,
156
+ ): LoadedChalinConfig {
157
+ const loaded = loadEffectiveConfig(options);
158
+ const overrides = { ...loaded.config.agents.modelOverrides };
159
+ if (model) overrides[agentRef] = model;
160
+ else delete overrides[agentRef];
161
+ const patch: Partial<ChalinConfig> = { agents: { ...loaded.config.agents, modelOverrides: overrides } };
162
+ return target === "project" ? writeProjectConfig(options, patch) : writeUserConfig(options, patch);
163
+ }
164
+
165
+ export function setAgentThinkingOverride(
166
+ options: ChalinPathsOptions,
167
+ agentRef: string,
168
+ thinking: AgentThinkingLevel | undefined,
169
+ target: Exclude<ModelPersistenceTarget, "session">,
170
+ ): LoadedChalinConfig {
171
+ const loaded = loadEffectiveConfig(options);
172
+ const overrides = { ...loaded.config.agents.thinkingOverrides };
173
+ if (thinking && thinking !== "inherit") overrides[agentRef] = thinking;
174
+ else delete overrides[agentRef];
175
+ const patch: Partial<ChalinConfig> = { agents: { ...loaded.config.agents, thinkingOverrides: overrides } };
176
+ return target === "project" ? writeProjectConfig(options, patch) : writeUserConfig(options, patch);
177
+ }
178
+
179
+ export function approvalDecision(config: ChalinConfig, route: RouteDecision): ApprovalDecision {
180
+ if (route.kind === "bypass" || route.kind === "memory-only") return { action: "allow", reason: "No subagent execution required." };
181
+ if (route.risk === "critical" && config.safety.blockCritical) {
182
+ return { action: "block", reason: "Critical routes are blocked by default safety policy." };
183
+ }
184
+ const threshold = config.autonomy === "low" ? "low" : config.safety.approvalRiskThreshold;
185
+ if (riskRank(route.risk) >= riskRank(threshold)) {
186
+ return { action: "ask", reason: `Route risk '${route.risk}' meets approval threshold '${threshold}'.` };
187
+ }
188
+ return { action: "allow", reason: `Route risk '${route.risk}' is below approval threshold '${threshold}'.` };
189
+ }
@@ -0,0 +1,190 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ export type DiscoveryEntryType = "file" | "dir" | "symlink" | "other";
5
+
6
+ export interface DiscoveryEntry {
7
+ path: string;
8
+ type: DiscoveryEntryType;
9
+ depth: number;
10
+ ext?: string;
11
+ sizeBytes?: number;
12
+ childCount?: number;
13
+ }
14
+
15
+ export interface ProjectDiscoveryIndex {
16
+ version: 1;
17
+ createdAt: string;
18
+ cwd: string;
19
+ entries: DiscoveryEntry[];
20
+ truncated: boolean;
21
+ ignoredDirs: string[];
22
+ extensionHistogram: Record<string, number>;
23
+ shallowFiles: string[];
24
+ instructionFiles: string[];
25
+ configLikeFiles: string[];
26
+ testLikeFiles: string[];
27
+ }
28
+
29
+ export interface ProjectDiscoveryOptions {
30
+ maxDepth?: number;
31
+ maxEntries?: number;
32
+ maxChildrenPerDir?: number;
33
+ }
34
+
35
+ const DEFAULT_MAX_DEPTH = 4;
36
+ const DEFAULT_MAX_ENTRIES = 450;
37
+ const DEFAULT_MAX_CHILDREN_PER_DIR = 120;
38
+ const IGNORED_DIRS = new Set([
39
+ ".git",
40
+ ".hg",
41
+ ".svn",
42
+ ".pi-chalin",
43
+ "node_modules",
44
+ "vendor",
45
+ "dist",
46
+ "build",
47
+ "coverage",
48
+ ".next",
49
+ ".nuxt",
50
+ ".turbo",
51
+ "target",
52
+ "__pycache__",
53
+ ]);
54
+
55
+ export function buildProjectDiscoveryIndex(cwdInput: string, options: ProjectDiscoveryOptions = {}): ProjectDiscoveryIndex {
56
+ const cwd = path.resolve(cwdInput);
57
+ const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
58
+ const maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
59
+ const maxChildrenPerDir = options.maxChildrenPerDir ?? DEFAULT_MAX_CHILDREN_PER_DIR;
60
+ const entries: DiscoveryEntry[] = [];
61
+ const ignoredDirs = new Set<string>();
62
+ let truncated = false;
63
+
64
+ const queue: Array<{ relativePath: string; depth: number }> = [{ relativePath: "", depth: 0 }];
65
+ while (queue.length > 0 && entries.length < maxEntries) {
66
+ const current = queue.shift()!;
67
+ const absolute = path.join(cwd, current.relativePath);
68
+ const children = safeReaddir(absolute).slice(0, maxChildrenPerDir);
69
+ if (children.length === maxChildrenPerDir) truncated = true;
70
+
71
+ for (const child of children) {
72
+ if (entries.length >= maxEntries) {
73
+ truncated = true;
74
+ break;
75
+ }
76
+ const relativePath = path.join(current.relativePath, child).replaceAll(path.sep, "/");
77
+ const childAbsolute = path.join(cwd, relativePath);
78
+ const stat = safeLstat(childAbsolute);
79
+ if (!stat) continue;
80
+ const type = entryType(stat);
81
+ const depth = current.depth + 1;
82
+ if (type === "dir" && IGNORED_DIRS.has(child)) {
83
+ ignoredDirs.add(relativePath);
84
+ continue;
85
+ }
86
+ const entry: DiscoveryEntry = {
87
+ path: relativePath,
88
+ type,
89
+ depth,
90
+ ...(type === "file" ? { ext: fileExt(child), sizeBytes: stat.size } : {}),
91
+ ...(type === "dir" ? { childCount: safeReaddir(childAbsolute).length } : {}),
92
+ };
93
+ entries.push(entry);
94
+ if (type === "dir" && depth < maxDepth) queue.push({ relativePath, depth });
95
+ }
96
+ }
97
+ if (queue.length > 0) truncated = true;
98
+
99
+ const files = entries.filter((entry) => entry.type === "file");
100
+ return {
101
+ version: 1,
102
+ createdAt: new Date().toISOString(),
103
+ cwd,
104
+ entries,
105
+ truncated,
106
+ ignoredDirs: [...ignoredDirs].sort(),
107
+ extensionHistogram: extensionHistogram(files),
108
+ shallowFiles: files.filter((entry) => entry.depth <= 2).map((entry) => entry.path).slice(0, 80),
109
+ instructionFiles: files.filter((entry) => isInstructionLike(entry.path)).map((entry) => entry.path).slice(0, 80),
110
+ configLikeFiles: files.filter((entry) => isConfigLike(entry.path)).map((entry) => entry.path).slice(0, 80),
111
+ testLikeFiles: files.filter((entry) => isTestLike(entry.path)).map((entry) => entry.path).slice(0, 80),
112
+ };
113
+ }
114
+
115
+ export function formatProjectDiscoveryIndex(index: ProjectDiscoveryIndex): string {
116
+ const dirs = index.entries.filter((entry) => entry.type === "dir").map((entry) => `${entry.path}/`).slice(0, 100);
117
+ const files = index.entries.filter((entry) => entry.type === "file").map((entry) => entry.path).slice(0, 160);
118
+ const histogram = Object.entries(index.extensionHistogram)
119
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
120
+ .slice(0, 18)
121
+ .map(([ext, count]) => `${ext}:${count}`)
122
+ .join(", ");
123
+ return [
124
+ "Project discovery index (raw, non-semantic):",
125
+ `- cwd: ${index.cwd}`,
126
+ `- entries: ${index.entries.length}${index.truncated ? " (truncated)" : ""}`,
127
+ index.ignoredDirs.length ? `- ignored dirs: ${index.ignoredDirs.join(", ")}` : undefined,
128
+ histogram ? `- extension histogram: ${histogram}` : undefined,
129
+ dirs.length ? `- directories: ${dirs.join(", ")}` : undefined,
130
+ index.instructionFiles.length ? `- instruction/JIT files: ${index.instructionFiles.join(", ")}` : undefined,
131
+ index.shallowFiles.length ? `- shallow files: ${index.shallowFiles.join(", ")}` : undefined,
132
+ index.configLikeFiles.length ? `- config-like files: ${index.configLikeFiles.join(", ")}` : undefined,
133
+ index.testLikeFiles.length ? `- test-like files: ${index.testLikeFiles.join(", ")}` : undefined,
134
+ files.length ? `- sampled files: ${files.join(", ")}` : undefined,
135
+ "Guidance: treat this as an index only. For repositories with AGENTS.md/CONTEXT.md/JIT files, read the root instruction file and relevant package instruction files before broad code reads; then verify claims with the smallest evidence set.",
136
+ ].filter((line): line is string => Boolean(line)).join("\n");
137
+ }
138
+
139
+ function safeReaddir(dir: string): string[] {
140
+ try {
141
+ return fs.readdirSync(dir).sort((a, b) => a.localeCompare(b));
142
+ } catch {
143
+ return [];
144
+ }
145
+ }
146
+
147
+ function safeLstat(filePath: string): fs.Stats | undefined {
148
+ try {
149
+ return fs.lstatSync(filePath);
150
+ } catch {
151
+ return undefined;
152
+ }
153
+ }
154
+
155
+ function entryType(stat: fs.Stats): DiscoveryEntryType {
156
+ if (stat.isSymbolicLink()) return "symlink";
157
+ if (stat.isDirectory()) return "dir";
158
+ if (stat.isFile()) return "file";
159
+ return "other";
160
+ }
161
+
162
+ function fileExt(fileName: string): string {
163
+ const ext = path.extname(fileName).toLowerCase();
164
+ return ext || "[none]";
165
+ }
166
+
167
+ function extensionHistogram(files: DiscoveryEntry[]): Record<string, number> {
168
+ const result: Record<string, number> = {};
169
+ for (const file of files) result[file.ext ?? "[none]"] = (result[file.ext ?? "[none]"] ?? 0) + 1;
170
+ return result;
171
+ }
172
+
173
+ function isConfigLike(filePath: string): boolean {
174
+ const base = path.basename(filePath).toLowerCase();
175
+ return /(^|[.-])(config|rc|lock|workspace|manifest|project|settings|schema)([.-]|$)/.test(base)
176
+ || ["makefile", "dockerfile", "readme.md", "agents.md", "package.json", "go.mod", "cargo.toml", "pyproject.toml", "pom.xml", "build.gradle", "composer.json", "gemfile"].includes(base)
177
+ || /\.(ya?ml|toml|json|ini|env|properties)$/i.test(base);
178
+ }
179
+
180
+ function isInstructionLike(filePath: string): boolean {
181
+ const base = path.basename(filePath).toLowerCase();
182
+ return ["agents.md", "claude.md", "context.md"].includes(base)
183
+ || /(^|\/)(docs\/adr|adr)\//i.test(filePath)
184
+ || /(^|\/)\.agents\/skills\/readme\.md$/i.test(filePath);
185
+ }
186
+
187
+ function isTestLike(filePath: string): boolean {
188
+ return /(^|\/)(test|tests|spec|__tests__|e2e)(\/|$)/i.test(filePath)
189
+ || /(?:^|[._-])(test|spec)\.[a-z0-9]+$/i.test(path.basename(filePath));
190
+ }
package/src/index.ts ADDED
@@ -0,0 +1,40 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { registerChalinAutoRouter } from "./autoroute.ts";
3
+ import { hideLegacyTopLevelChildSessions } from "./child-sessions.ts";
4
+ import { registerChalinCommands } from "./commands.ts";
5
+ import { registerChalinTools } from "./tools.ts";
6
+ import { setChalinStatus } from "./ui-status.ts";
7
+
8
+ const PI_CHALIN_CHILD_ENV = "PI_CHALIN_CHILD";
9
+ const PI_CHALIN_DISABLED_ENV = "PI_CHALIN_DISABLED";
10
+
11
+ export default function registerPiChalin(pi: ExtensionAPI): void {
12
+ if (process.env[PI_CHALIN_CHILD_ENV] === "1" || process.env[PI_CHALIN_DISABLED_ENV] === "1") return;
13
+
14
+ registerChalinCommands(pi);
15
+ registerChalinTools(pi);
16
+ registerChalinAutoRouter(pi);
17
+
18
+ pi.on("session_start", (_event, ctx) => {
19
+ void hideLegacyTopLevelChildSessions(ctx).then((result) => {
20
+ if (ctx.hasUI && result.moved.length > 0) {
21
+ ctx.ui.notify(`pi-chalin hid ${result.moved.length} legacy child session(s) from Pi resume.`, "info");
22
+ }
23
+ }).catch((error) => {
24
+ const message = error instanceof Error ? error.message : String(error);
25
+ console.warn(`pi-chalin legacy child session cleanup failed: ${message}`);
26
+ });
27
+ if (!ctx.hasUI) return;
28
+ setChalinStatus(ctx, { kind: "idle" });
29
+ });
30
+ }
31
+
32
+ export { PI_CHALIN_CHILD_ENV, PI_CHALIN_DISABLED_ENV };
33
+ export { AgentCatalog, parseFrontmatter } from "./agents.ts";
34
+ export { ArtifactStore } from "./artifacts.ts";
35
+ export { DEFAULT_CONFIG, approvalDecision, loadEffectiveConfig, setAgentModelOverride, setAgentThinkingOverride, writeProjectConfig, writeUserConfig } from "./config.ts";
36
+ export { ChalinKernel } from "./kernel.ts";
37
+ export { MemoryStore, createMemoryCandidate } from "./memory.ts";
38
+ export { MockWorkerRunner, SdkWorkerRunner, parseAgentOutput } from "./runner.ts";
39
+ export { resolveChalinPaths } from "./paths.ts";
40
+ export type { AgentDefinition, AgentMemoryPolicy, AgentThinkingLevel, RouteDecision, RoutePlan, RunState, MemoryCandidate, MemoryRecord } from "./schemas.ts";