pi-dynamic-help 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.
@@ -0,0 +1,80 @@
1
+ import { STATE_VERSION } from "../constants.js";
2
+ import { canonicalName, compactSourceRef, displayName, makeItemKey } from "../identity.js";
3
+ import type { HelpState, IndexedItem, LegacyHelpState, ResourceKind, ResourceScope } from "../types.js";
4
+
5
+ export function emptyState(): HelpState {
6
+ return { version: STATE_VERSION, items: {} };
7
+ }
8
+
9
+ export function legacySourceFromKey(key: string | undefined): string | undefined {
10
+ if (!key) return undefined;
11
+ const first = key.indexOf(":");
12
+ const last = key.lastIndexOf(":");
13
+ if (first < 0 || last <= first) return undefined;
14
+ return key.slice(first + 1, last);
15
+ }
16
+
17
+ function fallbackSourceLabel(kind: ResourceKind, sourceRef: string, scope?: ResourceScope): string {
18
+ if (sourceRef === "builtin") return kind === "tool" ? "内置工具" : "内置";
19
+ if (sourceRef === "sdk") return kind === "tool" ? "SDK 工具" : "SDK";
20
+ if (sourceRef.startsWith("pkg:")) return `npm:${sourceRef.slice("pkg:".length)}`;
21
+ if (sourceRef.startsWith("npm:")) return sourceRef;
22
+ if (sourceRef.startsWith("mcp:")) return sourceRef === "mcp:gateway" ? "内置网关" : "MCP 服务器";
23
+ if (sourceRef.startsWith("local:")) return scope === "project" ? "项目本地" : "本地包";
24
+ return sourceRef;
25
+ }
26
+
27
+ export function mergeItem(existing: IndexedItem | undefined, next: IndexedItem): IndexedItem {
28
+ if (!existing) return next;
29
+
30
+ return {
31
+ ...existing,
32
+ ...next,
33
+ description: next.description ?? existing.description,
34
+ sourceLabel: next.sourceLabel || existing.sourceLabel,
35
+ sourceRef: next.sourceRef || existing.sourceRef,
36
+ useCount: Math.max(existing.useCount ?? 0, next.useCount ?? 0),
37
+ lastUsedAt: Math.max(existing.lastUsedAt ?? 0, next.lastUsedAt ?? 0) || undefined,
38
+ lastSeenAt: Math.max(existing.lastSeenAt ?? 0, next.lastSeenAt ?? 0) || undefined,
39
+ pinned: Boolean(existing.pinned || next.pinned),
40
+ };
41
+ }
42
+
43
+ export function normalizeRawItem(raw: Partial<IndexedItem> & { key?: string; kind?: ResourceKind; name?: string; source?: string }): IndexedItem | undefined {
44
+ if (!raw.kind || !raw.name) return undefined;
45
+
46
+ const sourceRef = compactSourceRef(raw.sourceRef ?? raw.source ?? legacySourceFromKey(raw.key) ?? raw.packageName ?? raw.sourceLabel);
47
+ const name = canonicalName(raw.kind, raw.name);
48
+ const key = makeItemKey(raw.kind, name, sourceRef);
49
+ return {
50
+ key,
51
+ kind: raw.kind,
52
+ name,
53
+ displayName: raw.displayName ?? displayName(raw.kind, name),
54
+ description: raw.description,
55
+ sourceLabel: raw.sourceLabel || fallbackSourceLabel(raw.kind, sourceRef, raw.scope),
56
+ sourceRef,
57
+ scope: raw.scope,
58
+ packageName: raw.packageName,
59
+ useCount: raw.useCount ?? 0,
60
+ lastUsedAt: raw.lastUsedAt,
61
+ lastSeenAt: raw.lastSeenAt,
62
+ pinned: raw.pinned,
63
+ };
64
+ }
65
+
66
+ export function migrateState(state: LegacyHelpState | HelpState): HelpState {
67
+ const items: Record<string, IndexedItem> = {};
68
+
69
+ for (const raw of Object.values(state.items ?? {})) {
70
+ const next = normalizeRawItem(raw);
71
+ if (!next) continue;
72
+ items[next.key] = mergeItem(items[next.key], next);
73
+ }
74
+
75
+ return {
76
+ version: STATE_VERSION,
77
+ items,
78
+ lastRefresh: state.lastRefresh,
79
+ };
80
+ }
@@ -0,0 +1,70 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import type { HelpState, LegacyHelpState, PersistResult } from "../types.js";
4
+ import { emptyState, migrateState } from "./migrate.js";
5
+
6
+ export interface StatePaths {
7
+ stateDir: string;
8
+ stateFile: string;
9
+ legacyStateFile: string;
10
+ }
11
+
12
+ export function createStatePaths(agentDir: string): StatePaths {
13
+ const stateDir = join(agentDir, "state", "pi-dynamic-help");
14
+ return {
15
+ stateDir,
16
+ stateFile: join(stateDir, "state.json"),
17
+ legacyStateFile: join(agentDir, "state", "dynamic-help.json"),
18
+ };
19
+ }
20
+
21
+ function parseJsonFile<T>(path: string): { ok: true; value: T } | { ok: false; error: unknown } | { ok: undefined } {
22
+ if (!existsSync(path)) return { ok: undefined };
23
+ try {
24
+ return { ok: true, value: JSON.parse(readFileSync(path, "utf-8")) as T };
25
+ } catch (error) {
26
+ return { ok: false, error };
27
+ }
28
+ }
29
+
30
+ function timestamp(): string {
31
+ return new Date().toISOString().replace(/[-:]/g, "").replace(/\..*$/, "").replace("T", "-");
32
+ }
33
+
34
+ export function backupCorruptState(path: string): string | undefined {
35
+ if (!existsSync(path)) return undefined;
36
+ const backup = join(dirname(path), `state.corrupt-${timestamp()}.json`);
37
+ renameSync(path, backup);
38
+ return backup;
39
+ }
40
+
41
+ export function loadState(paths: StatePaths): { state: HelpState; warnings: string[] } {
42
+ const warnings: string[] = [];
43
+ const current = parseJsonFile<LegacyHelpState | HelpState>(paths.stateFile);
44
+ if (current.ok === true) return { state: migrateState(current.value), warnings };
45
+ if (current.ok === false) {
46
+ try {
47
+ const backup = backupCorruptState(paths.stateFile);
48
+ warnings.push(`状态文件损坏,已备份为 ${backup}`);
49
+ } catch (error) {
50
+ warnings.push(`状态文件损坏,且备份失败:${String(error)}`);
51
+ }
52
+ }
53
+
54
+ const legacy = parseJsonFile<LegacyHelpState>(paths.legacyStateFile);
55
+ if (legacy.ok === true) return { state: migrateState(legacy.value), warnings };
56
+ if (legacy.ok === false) warnings.push("旧状态文件无法解析,已忽略");
57
+ return { state: emptyState(), warnings };
58
+ }
59
+
60
+ export function saveState(paths: StatePaths, state: HelpState): PersistResult {
61
+ try {
62
+ mkdirSync(paths.stateDir, { recursive: true });
63
+ const tmp = `${paths.stateFile}.tmp-${process.pid}-${Date.now()}`;
64
+ writeFileSync(tmp, `${JSON.stringify(state, null, 2)}\n`);
65
+ renameSync(tmp, paths.stateFile);
66
+ return { ok: true };
67
+ } catch (error) {
68
+ return { ok: false, warning: `帮助状态保存失败:${String(error)}` };
69
+ }
70
+ }
@@ -0,0 +1,60 @@
1
+ import { MAX_ITEMS } from "../constants.js";
2
+ import { normalizeSearch } from "../identity.js";
3
+ import type { HelpState, IndexedItem, ResourceKind } from "../types.js";
4
+ import { mergeItem } from "./migrate.js";
5
+
6
+ export function sortItems(a: IndexedItem, b: IndexedItem): number {
7
+ if ((a.pinned ? 1 : 0) !== (b.pinned ? 1 : 0)) return (b.pinned ? 1 : 0) - (a.pinned ? 1 : 0);
8
+ if ((a.useCount ?? 0) !== (b.useCount ?? 0)) return (b.useCount ?? 0) - (a.useCount ?? 0);
9
+ if ((a.lastUsedAt ?? 0) !== (b.lastUsedAt ?? 0)) return (b.lastUsedAt ?? 0) - (a.lastUsedAt ?? 0);
10
+ return (a.displayName ?? a.name).localeCompare(b.displayName ?? b.name);
11
+ }
12
+
13
+ export function upsertItem(state: HelpState, item: IndexedItem): IndexedItem {
14
+ const merged = mergeItem(state.items[item.key], item);
15
+ state.items[item.key] = merged;
16
+ return merged;
17
+ }
18
+
19
+ export function recordUsage(state: HelpState, key: string, now = Date.now()): void {
20
+ const item = state.items[key];
21
+ if (!item) return;
22
+ item.useCount = (item.useCount ?? 0) + 1;
23
+ item.lastUsedAt = now;
24
+ item.lastSeenAt = now;
25
+ }
26
+
27
+ export function recordUsageByName(state: HelpState, kind: ResourceKind, name: string, now = Date.now()): void {
28
+ const matches = Object.values(state.items).filter((item) => item.kind === kind && (item.name === name || item.displayName === name));
29
+ if (matches.length === 0) return;
30
+ const target = [...matches].sort(sortItems)[0];
31
+ if (!target) return;
32
+ recordUsage(state, target.key, now);
33
+ }
34
+
35
+ export function findMatches(state: HelpState, query: string): IndexedItem[] {
36
+ const normalizedQuery = normalizeSearch(query);
37
+ if (!normalizedQuery) return [];
38
+ return Object.values(state.items)
39
+ .filter((item) => normalizeSearch([item.name, item.displayName, item.description, item.sourceLabel, item.packageName, item.sourceRef, item.kind].join(" ")).includes(normalizedQuery))
40
+ .sort(sortItems);
41
+ }
42
+
43
+ export function pruneState(state: HelpState): void {
44
+ const items = Object.values(state.items);
45
+ if (items.length <= MAX_ITEMS) return;
46
+
47
+ const removable = items
48
+ .filter((item) => !item.pinned)
49
+ .sort((a, b) => {
50
+ if ((a.useCount ?? 0) !== (b.useCount ?? 0)) {
51
+ return (a.useCount ?? 0) - (b.useCount ?? 0);
52
+ }
53
+ return (a.lastUsedAt ?? 0) - (b.lastUsedAt ?? 0);
54
+ });
55
+
56
+ while (Object.keys(state.items).length > MAX_ITEMS && removable.length > 0) {
57
+ const victim = removable.shift();
58
+ if (victim) delete state.items[victim.key];
59
+ }
60
+ }
package/src/types.ts ADDED
@@ -0,0 +1,56 @@
1
+ export type ResourceKind = "command" | "tool" | "package" | "mcp";
2
+
3
+ export type ResourceScope = "project" | "user" | "temporary" | "builtin";
4
+
5
+ export interface IndexedItem {
6
+ key: string;
7
+ kind: ResourceKind;
8
+ name: string;
9
+ displayName?: string;
10
+ description?: string;
11
+ sourceLabel: string;
12
+ sourceRef: string;
13
+ scope?: ResourceScope;
14
+ packageName?: string;
15
+ useCount: number;
16
+ lastUsedAt?: number;
17
+ lastSeenAt?: number;
18
+ pinned?: boolean;
19
+ }
20
+
21
+ export interface HelpState {
22
+ version: 3;
23
+ items: Record<string, IndexedItem>;
24
+ lastRefresh?: number;
25
+ }
26
+
27
+ export interface LegacyHelpState {
28
+ version?: number;
29
+ items?: Record<string, Partial<IndexedItem> & { key?: string; kind?: ResourceKind; name?: string; source?: string; origin?: string }>;
30
+ lastRefresh?: number;
31
+ }
32
+
33
+ export interface McpServerEntry {
34
+ name: string;
35
+ mode: string;
36
+ lifecycle?: string;
37
+ command?: string;
38
+ url?: string;
39
+ }
40
+
41
+ export interface PackageEntry {
42
+ name: string;
43
+ scope: ResourceScope;
44
+ }
45
+
46
+ export interface ResourceIndex {
47
+ commands: IndexedItem[];
48
+ tools: IndexedItem[];
49
+ mcp: IndexedItem[];
50
+ packages: IndexedItem[];
51
+ }
52
+
53
+ export interface PersistResult {
54
+ ok: boolean;
55
+ warning?: string;
56
+ }