@xpufx/paseo-forges 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.
- package/LICENSE +21 -0
- package/README.md +109 -0
- package/client/active-forge.ts +47 -0
- package/client/board-alert.tsx +236 -0
- package/client/foreign-link.tsx +33 -0
- package/client/hook-queue-panel.tsx +412 -0
- package/client/issues-pill.tsx +1976 -0
- package/client/label-chip.tsx +116 -0
- package/client/linkifier.tsx +280 -0
- package/client/pill-label.ts +183 -0
- package/client/vendor/paseo-plugin-helper/command-center.ts +43 -0
- package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +493 -0
- package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +250 -0
- package/client/vendor/paseo-plugin-helper/components/Badge.tsx +156 -0
- package/client/vendor/paseo-plugin-helper/components/Button.tsx +178 -0
- package/client/vendor/paseo-plugin-helper/components/Card.tsx +225 -0
- package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +196 -0
- package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +277 -0
- package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +172 -0
- package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +180 -0
- package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +200 -0
- package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +97 -0
- package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +70 -0
- package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +73 -0
- package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +446 -0
- package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +247 -0
- package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +117 -0
- package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +53 -0
- package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +118 -0
- package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/components/Select.tsx +215 -0
- package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +319 -0
- package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +150 -0
- package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +163 -0
- package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +157 -0
- package/client/vendor/paseo-plugin-helper/components/index.ts +25 -0
- package/client/vendor/paseo-plugin-helper/custom-pills.tsx +224 -0
- package/client/vendor/paseo-plugin-helper/forge-icon.tsx +79 -0
- package/client/vendor/paseo-plugin-helper/host.ts +277 -0
- package/client/vendor/paseo-plugin-helper/icon.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/index.ts +28 -0
- package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +49 -0
- package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +103 -0
- package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +65 -0
- package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +378 -0
- package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +49 -0
- package/client/vendor/paseo-plugin-helper/layout/Row.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/layout/index.ts +7 -0
- package/client/vendor/paseo-plugin-helper/panel.tsx +81 -0
- package/client/vendor/paseo-plugin-helper/pill.tsx +884 -0
- package/client/vendor/paseo-plugin-helper/query-refresh.ts +79 -0
- package/client/vendor/paseo-plugin-helper/query.ts +66 -0
- package/client/vendor/paseo-plugin-helper/settings-screen.tsx +372 -0
- package/client/vendor/paseo-plugin-helper/settings.ts +181 -0
- package/client/vendor/paseo-plugin-helper/shared-settings.ts +46 -0
- package/client/vendor/paseo-plugin-helper/snapshot.ts +68 -0
- package/client/vendor/paseo-plugin-helper/surface.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/theme/color-utils.ts +118 -0
- package/client/vendor/paseo-plugin-helper/theme/flair.ts +76 -0
- package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +121 -0
- package/client/vendor/paseo-plugin-helper/theme/index.ts +7 -0
- package/client/vendor/paseo-plugin-helper/theme/provider.tsx +214 -0
- package/client/vendor/paseo-plugin-helper/theme/responsive.ts +213 -0
- package/client/vendor/paseo-plugin-helper/theme/tokens.ts +161 -0
- package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +57 -0
- package/client/vendor/paseo-plugin-helper/utils/clipboard.ts +149 -0
- package/client/vendor/paseo-plugin-helper/utils/haptics.ts +34 -0
- package/client/webhook-card.tsx +198 -0
- package/docs/specs/forge-workflow-gui.md +820 -0
- package/docs/workflow.md +339 -0
- package/examples/README.md +108 -0
- package/examples/hook-service/README.md +66 -0
- package/examples/hook-service/forge-hook.service +28 -0
- package/examples/hook-service/hook-server.mjs +265 -0
- package/examples/hook-service/hook.env.example +21 -0
- package/examples/labels/README.md +64 -0
- package/examples/labels/label-base.yaml +117 -0
- package/examples/skills/coding-agent/SKILL.md +262 -0
- package/examples/skills/coding-agent-fgjx/SKILL.md +271 -0
- package/examples/skills/orchestrator/SKILL.md +133 -0
- package/examples/skills/orchestrator-fgjx/SKILL.md +139 -0
- package/examples/tools/README.md +68 -0
- package/examples/tools/fgjx +464 -0
- package/package.json +38 -0
- package/paseo-plugin.json +4 -0
- package/server/forge-client.ts +484 -0
- package/server/forge-guard.ts +70 -0
- package/server/git-origin.ts +70 -0
- package/server/hook-queue.ts +127 -0
- package/server/issues.ts +542 -0
- package/server/settings.ts +52 -0
- package/server/vendor/paseo-plugin-helper/agent.ts +85 -0
- package/server/vendor/paseo-plugin-helper/custom-pills.ts +344 -0
- package/server/vendor/paseo-plugin-helper/index.ts +18 -0
- package/server/vendor/paseo-plugin-helper/jsonc.ts +78 -0
- package/server/vendor/paseo-plugin-helper/logger.ts +210 -0
- package/server/vendor/paseo-plugin-helper/mcp-config.ts +367 -0
- package/server/vendor/paseo-plugin-helper/mcp-injection.ts +85 -0
- package/server/vendor/paseo-plugin-helper/network.ts +91 -0
- package/server/vendor/paseo-plugin-helper/plugins.ts +160 -0
- package/server/vendor/paseo-plugin-helper/process.ts +186 -0
- package/server/vendor/paseo-plugin-helper/redact.ts +86 -0
- package/server/vendor/paseo-plugin-helper/rpc-guard.ts +77 -0
- package/server/vendor/paseo-plugin-helper/settings.ts +97 -0
- package/server/vendor/paseo-plugin-helper/shared-settings.ts +243 -0
- package/server/vendor/paseo-plugin-helper/storage.ts +244 -0
- package/server/vendor/paseo-plugin-helper/system.ts +128 -0
- package/server/vendor/paseo-plugin-helper/task.ts +116 -0
- package/server/vendor/paseo-plugin-helper/version.ts +153 -0
- package/server/vendor/paseo-plugin-helper/workspace-beacon.ts +418 -0
- package/shared/hook-queue.ts +140 -0
- package/shared/issues.ts +1750 -0
- package/shared/vendor/paseo-plugin-helper/README.md +11 -0
- package/shared/vendor/paseo-plugin-helper/async.ts +35 -0
- package/shared/vendor/paseo-plugin-helper/custom-pills.ts +169 -0
- package/shared/vendor/paseo-plugin-helper/forge.ts +110 -0
- package/shared/vendor/paseo-plugin-helper/formatters.ts +271 -0
- package/shared/vendor/paseo-plugin-helper/highlight.ts +184 -0
- package/shared/vendor/paseo-plugin-helper/index.ts +10 -0
- package/shared/vendor/paseo-plugin-helper/rpc.ts +72 -0
- package/shared/vendor/paseo-plugin-helper/settings.ts +138 -0
- package/shared/vendor/paseo-plugin-helper/suite-settings.ts +17 -0
- package/shared/vendor/paseo-plugin-helper/suppressed.ts +31 -0
- package/shared/vendor/paseo-plugin-helper/types.ts +36 -0
- package/shared/version.ts +2 -0
- package/shared/webhook.ts +223 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PluginStorage,
|
|
3
|
+
createPluginLogger,
|
|
4
|
+
createSettingsHandlers,
|
|
5
|
+
} from "paseo-plugin-helper/server";
|
|
6
|
+
import {
|
|
7
|
+
FORGES_PLUGIN_ID,
|
|
8
|
+
activeForgeForDirectory,
|
|
9
|
+
forgeSettingsContract,
|
|
10
|
+
type ForgeSettings,
|
|
11
|
+
} from "../shared/issues.js";
|
|
12
|
+
|
|
13
|
+
export const log = createPluginLogger(FORGES_PLUGIN_ID);
|
|
14
|
+
|
|
15
|
+
export const forgeStorage = new PluginStorage<ForgeSettings>(
|
|
16
|
+
FORGES_PLUGIN_ID,
|
|
17
|
+
"settings.json",
|
|
18
|
+
{ schema: forgeSettingsContract.schema },
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
export const settingsHandlers = createSettingsHandlers(
|
|
22
|
+
forgeSettingsContract,
|
|
23
|
+
forgeStorage,
|
|
24
|
+
{
|
|
25
|
+
onUpdate: (newSettings) => {
|
|
26
|
+
log.info("Forge settings updated via RPC:", newSettings);
|
|
27
|
+
},
|
|
28
|
+
onReset: () => {
|
|
29
|
+
log.info("Forge settings reset to default values");
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Explicit forge target selected for a workspace directory, if any. Reflects
|
|
36
|
+
* the per-workspace `activeForgeByDirectory` choice, falling back to the legacy
|
|
37
|
+
* single remote override when no selection was ever made (issue #137).
|
|
38
|
+
*/
|
|
39
|
+
export async function storedForgeSelection(
|
|
40
|
+
directory: string | undefined,
|
|
41
|
+
): Promise<string | undefined> {
|
|
42
|
+
if (!directory) return undefined;
|
|
43
|
+
const settings = await forgeStorage.readAsync();
|
|
44
|
+
return activeForgeForDirectory(settings, directory) ?? undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Daemon-side API token for a forge host. Never leaves the server. */
|
|
48
|
+
export async function tokenForHost(host: string): Promise<string | undefined> {
|
|
49
|
+
const settings = await forgeStorage.readAsync();
|
|
50
|
+
const token = settings.tokensByHost?.[host];
|
|
51
|
+
return typeof token === "string" && token.trim() ? token.trim() : undefined;
|
|
52
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { safeExec } from "./process";
|
|
2
|
+
|
|
3
|
+
export interface AgentIdentity {
|
|
4
|
+
id?: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
model?: string;
|
|
7
|
+
provider?: string;
|
|
8
|
+
repo?: string;
|
|
9
|
+
branch?: string;
|
|
10
|
+
envelopeText?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AgentIdentityOptions {
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
envelopeCommand?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const ENV_KEYS = [
|
|
19
|
+
"AGENT_ID",
|
|
20
|
+
"AGENT_NAME",
|
|
21
|
+
"AGENT_MODEL",
|
|
22
|
+
"AGENT_PROVIDER",
|
|
23
|
+
"PASEO_AGENT_ID",
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
function readEnvIdentity(env: NodeJS.ProcessEnv = process.env): AgentIdentity | null {
|
|
27
|
+
const id = env.PASEO_AGENT_ID || env.AGENT_ID || undefined;
|
|
28
|
+
const name = env.AGENT_NAME || undefined;
|
|
29
|
+
const model = env.AGENT_MODEL || undefined;
|
|
30
|
+
const provider = env.AGENT_PROVIDER || undefined;
|
|
31
|
+
if (!id && !name && !model && !provider) return null;
|
|
32
|
+
return { id, name, model, provider };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeEnvelopeJson(raw: unknown): AgentIdentity | null {
|
|
36
|
+
if (!raw || typeof raw !== "object") return null;
|
|
37
|
+
const o = raw as Record<string, unknown>;
|
|
38
|
+
const str = (v: unknown): string | undefined =>
|
|
39
|
+
typeof v === "string" && v.length > 0 ? v : undefined;
|
|
40
|
+
const identity: AgentIdentity = {
|
|
41
|
+
id: str(o.id) ?? str(o.agentId),
|
|
42
|
+
name: str(o.name) ?? str(o.agentName),
|
|
43
|
+
model: str(o.model),
|
|
44
|
+
provider: str(o.provider),
|
|
45
|
+
repo: str(o.repo) ?? str(o.workspace),
|
|
46
|
+
branch: str(o.branch),
|
|
47
|
+
envelopeText: str(o.envelopeText) ?? str(o.envelope),
|
|
48
|
+
};
|
|
49
|
+
if (!identity.id && !identity.name && !identity.model && !identity.provider && !identity.repo && !identity.branch) return null;
|
|
50
|
+
return identity;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function getAgentIdentity(
|
|
54
|
+
options: AgentIdentityOptions = {},
|
|
55
|
+
): Promise<AgentIdentity | null> {
|
|
56
|
+
const timeoutMs = options.timeoutMs ?? 5000;
|
|
57
|
+
const command = options.envelopeCommand ?? "xpufx-tool envelope --format json";
|
|
58
|
+
const envIdentity = readEnvIdentity();
|
|
59
|
+
void ENV_KEYS;
|
|
60
|
+
try {
|
|
61
|
+
const result = await safeExec(command, { timeoutMs });
|
|
62
|
+
if (result.code !== 0) return envIdentity;
|
|
63
|
+
const text = result.stdout.trim();
|
|
64
|
+
if (!text) return envIdentity;
|
|
65
|
+
try {
|
|
66
|
+
const parsed: unknown = JSON.parse(text);
|
|
67
|
+
const cliIdentity = normalizeEnvelopeJson(parsed);
|
|
68
|
+
if (!cliIdentity) return envIdentity;
|
|
69
|
+
if (!envIdentity) return cliIdentity;
|
|
70
|
+
return {
|
|
71
|
+
id: envIdentity.id ?? cliIdentity.id,
|
|
72
|
+
name: envIdentity.name ?? cliIdentity.name,
|
|
73
|
+
model: envIdentity.model ?? cliIdentity.model,
|
|
74
|
+
provider: envIdentity.provider ?? cliIdentity.provider,
|
|
75
|
+
repo: cliIdentity.repo,
|
|
76
|
+
branch: cliIdentity.branch,
|
|
77
|
+
envelopeText: cliIdentity.envelopeText,
|
|
78
|
+
};
|
|
79
|
+
} catch {
|
|
80
|
+
return envIdentity;
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
return envIdentity;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parseJsonc } from "./jsonc";
|
|
4
|
+
import { safeExec } from "./process";
|
|
5
|
+
import type { PluginLogger } from "./logger";
|
|
6
|
+
import {
|
|
7
|
+
CustomPillDefinitionSchema,
|
|
8
|
+
type CustomPillDefinition,
|
|
9
|
+
type CustomPillState,
|
|
10
|
+
resolveCustomPillStatus,
|
|
11
|
+
parseNumericPillValue,
|
|
12
|
+
formatPillDisplay,
|
|
13
|
+
} from "../../../shared/vendor/paseo-plugin-helper/custom-pills";
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Discovers and validates all custom pill configuration files (.json / .jsonc)
|
|
18
|
+
* from a directory (e.g. ~/.paseo/top/pills or ~/.paseo/custom-pills).
|
|
19
|
+
*/
|
|
20
|
+
export async function discoverCustomPillConfigs(
|
|
21
|
+
dirPath: string,
|
|
22
|
+
logger?: PluginLogger,
|
|
23
|
+
): Promise<CustomPillDefinition[]> {
|
|
24
|
+
try {
|
|
25
|
+
if (!fs.existsSync(dirPath)) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
|
30
|
+
const configs: CustomPillDefinition[] = [];
|
|
31
|
+
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
if (
|
|
34
|
+
!entry.isFile() ||
|
|
35
|
+
(!entry.name.endsWith(".json") && !entry.name.endsWith(".jsonc"))
|
|
36
|
+
) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const filePath = path.join(dirPath, entry.name);
|
|
41
|
+
try {
|
|
42
|
+
const rawContent = await fs.promises.readFile(filePath, "utf-8");
|
|
43
|
+
const parsed = parseJsonc(rawContent);
|
|
44
|
+
|
|
45
|
+
const result = CustomPillDefinitionSchema.safeParse(parsed);
|
|
46
|
+
if (result.success) {
|
|
47
|
+
configs.push({ ...result.data, sourceFile: filePath });
|
|
48
|
+
} else {
|
|
49
|
+
logger?.warn(
|
|
50
|
+
`Invalid custom pill config in ${entry.name}: ${result.error.issues.map((i) => i.message).join(", ")}`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
} catch (err) {
|
|
54
|
+
logger?.warn(
|
|
55
|
+
`Failed to read custom pill config from ${entry.name}: ${err instanceof Error ? err.message : String(err)}`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return configs;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
logger?.warn(
|
|
63
|
+
`Error reading custom pills directory ${dirPath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
64
|
+
);
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface CustomPillPollerOptions {
|
|
70
|
+
/**
|
|
71
|
+
* Initial list of custom pill definitions.
|
|
72
|
+
*/
|
|
73
|
+
pills?: CustomPillDefinition[];
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Optional directory to discover .json / .jsonc configs from.
|
|
77
|
+
*/
|
|
78
|
+
configDir?: string;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Custom environment variables passed to all executed commands
|
|
82
|
+
* (e.g. PASEO_AGENT_ID, PASEO_WORKSPACE_ID).
|
|
83
|
+
*/
|
|
84
|
+
env?: Record<string, string>;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Working directory for executed commands. Defaults to process.cwd().
|
|
88
|
+
*/
|
|
89
|
+
cwd?: string;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Optional structured logger.
|
|
93
|
+
*/
|
|
94
|
+
logger?: PluginLogger;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Callback fired whenever any custom pill state changes.
|
|
98
|
+
*/
|
|
99
|
+
onUpdate?: (states: CustomPillState[]) => void;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Managed server poller for user-defined declarative custom metric pills.
|
|
104
|
+
* Periodically executes shell commands, computes statuses via thresholds,
|
|
105
|
+
* and maintains reactive live state.
|
|
106
|
+
*/
|
|
107
|
+
export class CustomPillPoller {
|
|
108
|
+
private pills = new Map<string, CustomPillDefinition>();
|
|
109
|
+
private states = new Map<string, CustomPillState>();
|
|
110
|
+
private timers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
111
|
+
private inFlight = new Set<string>();
|
|
112
|
+
private running = false;
|
|
113
|
+
private options: CustomPillPollerOptions;
|
|
114
|
+
|
|
115
|
+
constructor(options: CustomPillPollerOptions = {}) {
|
|
116
|
+
this.options = options;
|
|
117
|
+
if (options.pills) {
|
|
118
|
+
for (const pill of options.pills) {
|
|
119
|
+
this.pills.set(pill.id, pill);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Starts the polling loops for all configured custom pills.
|
|
126
|
+
*/
|
|
127
|
+
async start(): Promise<void> {
|
|
128
|
+
if (this.running) return;
|
|
129
|
+
this.running = true;
|
|
130
|
+
|
|
131
|
+
if (this.options.configDir) {
|
|
132
|
+
const discovered = await discoverCustomPillConfigs(
|
|
133
|
+
this.options.configDir,
|
|
134
|
+
this.options.logger,
|
|
135
|
+
);
|
|
136
|
+
for (const pill of discovered) {
|
|
137
|
+
this.pills.set(pill.id, pill);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const pill of this.pills.values()) {
|
|
142
|
+
if (pill.enabled) {
|
|
143
|
+
this.schedulePill(pill, 0);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Manually triggers an immediate execution of a single custom pill.
|
|
150
|
+
*/
|
|
151
|
+
async pollPill(pillId: string): Promise<CustomPillState | undefined> {
|
|
152
|
+
const pill = this.pills.get(pillId);
|
|
153
|
+
if (!pill) return undefined;
|
|
154
|
+
if (this.inFlight.has(pillId)) {
|
|
155
|
+
return this.states.get(pillId);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this.inFlight.add(pillId);
|
|
159
|
+
try {
|
|
160
|
+
const result = await safeExec(pill.command, {
|
|
161
|
+
timeoutMs: pill.timeoutMs,
|
|
162
|
+
env: { ...process.env, ...this.options.env },
|
|
163
|
+
cwd: this.options.cwd,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const rawValue = result.stdout || result.stderr || "";
|
|
167
|
+
const numericValue = parseNumericPillValue(rawValue);
|
|
168
|
+
const status = resolveCustomPillStatus(numericValue, pill.thresholds);
|
|
169
|
+
const displayValue = formatPillDisplay(rawValue, pill.prefix, pill.suffix);
|
|
170
|
+
|
|
171
|
+
const state: CustomPillState = {
|
|
172
|
+
id: pill.id,
|
|
173
|
+
title: pill.title,
|
|
174
|
+
compactTitle: pill.compactTitle,
|
|
175
|
+
icon: pill.icon,
|
|
176
|
+
compactIcon: pill.compactIcon,
|
|
177
|
+
rawValue,
|
|
178
|
+
displayValue,
|
|
179
|
+
numericValue,
|
|
180
|
+
status,
|
|
181
|
+
lastUpdated: Date.now(),
|
|
182
|
+
sourceFile: pill.sourceFile,
|
|
183
|
+
modalTitle: pill.modal?.title ?? pill.title,
|
|
184
|
+
modalDescription: pill.modal?.description,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
this.states.set(pill.id, state);
|
|
188
|
+
this.notifyUpdate();
|
|
189
|
+
return state;
|
|
190
|
+
} catch (err) {
|
|
191
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
192
|
+
this.options.logger?.warn(`Custom pill '${pill.id}' execution failed: ${errorMsg}`);
|
|
193
|
+
|
|
194
|
+
const previous = this.states.get(pill.id);
|
|
195
|
+
const state: CustomPillState = {
|
|
196
|
+
id: pill.id,
|
|
197
|
+
title: pill.title,
|
|
198
|
+
compactTitle: pill.compactTitle,
|
|
199
|
+
icon: pill.icon,
|
|
200
|
+
compactIcon: pill.compactIcon,
|
|
201
|
+
rawValue: previous?.rawValue ?? "ERR",
|
|
202
|
+
displayValue: previous?.displayValue ?? "ERR",
|
|
203
|
+
status: "danger",
|
|
204
|
+
lastUpdated: Date.now(),
|
|
205
|
+
error: errorMsg,
|
|
206
|
+
sourceFile: pill.sourceFile,
|
|
207
|
+
modalTitle: pill.modal?.title ?? pill.title,
|
|
208
|
+
modalDescription: pill.modal?.description,
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
this.states.set(pill.id, state);
|
|
212
|
+
this.notifyUpdate();
|
|
213
|
+
return state;
|
|
214
|
+
} finally {
|
|
215
|
+
this.inFlight.delete(pillId);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Executes the on-demand drilldown command configured in pill.modal.command.
|
|
221
|
+
*/
|
|
222
|
+
async runModalCommand(
|
|
223
|
+
pillId: string,
|
|
224
|
+
): Promise<{ output?: string; error?: string }> {
|
|
225
|
+
const pill = this.pills.get(pillId);
|
|
226
|
+
if (!pill) {
|
|
227
|
+
return { error: `Custom pill '${pillId}' not found` };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const commandToRun = pill.modal?.command ?? pill.command;
|
|
231
|
+
try {
|
|
232
|
+
const result = await safeExec(commandToRun, {
|
|
233
|
+
timeoutMs: pill.timeoutMs,
|
|
234
|
+
env: { ...process.env, ...this.options.env },
|
|
235
|
+
cwd: this.options.cwd,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const output = result.stdout || result.stderr || "No output";
|
|
239
|
+
const existing = this.states.get(pillId);
|
|
240
|
+
if (existing) {
|
|
241
|
+
existing.modalOutput = output;
|
|
242
|
+
existing.modalLastUpdated = Date.now();
|
|
243
|
+
delete existing.modalError;
|
|
244
|
+
this.notifyUpdate();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return { output };
|
|
248
|
+
} catch (err) {
|
|
249
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
250
|
+
const existing = this.states.get(pillId);
|
|
251
|
+
if (existing) {
|
|
252
|
+
existing.modalError = errorMsg;
|
|
253
|
+
this.notifyUpdate();
|
|
254
|
+
}
|
|
255
|
+
return { error: errorMsg };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Updates or reconciles the list of pill definitions dynamically.
|
|
261
|
+
*/
|
|
262
|
+
updatePills(newPills: CustomPillDefinition[]): void {
|
|
263
|
+
const nextIds = new Set(newPills.map((p) => p.id));
|
|
264
|
+
|
|
265
|
+
// Remove obsolete pills
|
|
266
|
+
for (const [id, timer] of this.timers.entries()) {
|
|
267
|
+
if (!nextIds.has(id)) {
|
|
268
|
+
clearTimeout(timer);
|
|
269
|
+
this.timers.delete(id);
|
|
270
|
+
this.pills.delete(id);
|
|
271
|
+
this.states.delete(id);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Add or update pills
|
|
276
|
+
for (const pill of newPills) {
|
|
277
|
+
this.pills.set(pill.id, pill);
|
|
278
|
+
const currentTimer = this.timers.get(pill.id);
|
|
279
|
+
if (currentTimer) {
|
|
280
|
+
clearTimeout(currentTimer);
|
|
281
|
+
this.timers.delete(pill.id);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (this.running && pill.enabled) {
|
|
285
|
+
this.schedulePill(pill, 0);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
this.notifyUpdate();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Returns live state for a single custom pill.
|
|
294
|
+
*/
|
|
295
|
+
getState(pillId: string): CustomPillState | undefined {
|
|
296
|
+
return this.states.get(pillId);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Returns live states for all custom pills.
|
|
301
|
+
*/
|
|
302
|
+
getAllStates(): CustomPillState[] {
|
|
303
|
+
return Array.from(this.states.values());
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Stops all active polling loops and clears resources.
|
|
308
|
+
*/
|
|
309
|
+
stop(): void {
|
|
310
|
+
this.running = false;
|
|
311
|
+
for (const timer of this.timers.values()) {
|
|
312
|
+
clearTimeout(timer);
|
|
313
|
+
}
|
|
314
|
+
this.timers.clear();
|
|
315
|
+
this.inFlight.clear();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private schedulePill(pill: CustomPillDefinition, delayMs: number): void {
|
|
319
|
+
if (!this.running) return;
|
|
320
|
+
|
|
321
|
+
// @ts-ignore
|
|
322
|
+
const timer = setTimeout(async () => {
|
|
323
|
+
await this.pollPill(pill.id);
|
|
324
|
+
if (this.running && this.pills.has(pill.id)) {
|
|
325
|
+
const nextPill = this.pills.get(pill.id);
|
|
326
|
+
if (nextPill?.enabled) {
|
|
327
|
+
this.schedulePill(nextPill, nextPill.intervalMs);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}, delayMs as any);
|
|
331
|
+
|
|
332
|
+
this.timers.set(pill.id, timer);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private notifyUpdate(): void {
|
|
336
|
+
if (this.options.onUpdate) {
|
|
337
|
+
try {
|
|
338
|
+
this.options.onUpdate(this.getAllStates());
|
|
339
|
+
} catch {
|
|
340
|
+
// Prevent callback errors from disrupting poller
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export * from "./storage";
|
|
2
|
+
export * from "./settings";
|
|
3
|
+
export * from "./shared-settings";
|
|
4
|
+
export * from "./jsonc";
|
|
5
|
+
export * from "./redact";
|
|
6
|
+
export * from "./process";
|
|
7
|
+
export * from "./system";
|
|
8
|
+
export * from "./logger";
|
|
9
|
+
export * from "./version";
|
|
10
|
+
export * from "./network";
|
|
11
|
+
export * from "./task";
|
|
12
|
+
export * from "./mcp-config";
|
|
13
|
+
export * from "./mcp-injection";
|
|
14
|
+
export * from "./plugins";
|
|
15
|
+
export * from "./custom-pills";
|
|
16
|
+
export * from "./rpc-guard";
|
|
17
|
+
export * from "./agent";
|
|
18
|
+
export * from "./workspace-beacon";
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strips single-line and multi-line comments and trailing commas from a JSONC string
|
|
3
|
+
* without altering strings or URLs.
|
|
4
|
+
*/
|
|
5
|
+
export function stripJsonComments(text: string): string {
|
|
6
|
+
let insideString = false;
|
|
7
|
+
let stringDelimiter = "";
|
|
8
|
+
let isEscaped = false;
|
|
9
|
+
let result = "";
|
|
10
|
+
|
|
11
|
+
for (let i = 0; i < text.length; i++) {
|
|
12
|
+
const char = text[i];
|
|
13
|
+
const nextChar = text[i + 1];
|
|
14
|
+
|
|
15
|
+
if (insideString) {
|
|
16
|
+
result += char;
|
|
17
|
+
if (isEscaped) {
|
|
18
|
+
isEscaped = false;
|
|
19
|
+
} else if (char === "\\") {
|
|
20
|
+
isEscaped = true;
|
|
21
|
+
} else if (char === stringDelimiter) {
|
|
22
|
+
insideString = false;
|
|
23
|
+
}
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (char === '"' || char === "'") {
|
|
28
|
+
insideString = true;
|
|
29
|
+
stringDelimiter = char;
|
|
30
|
+
result += char;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Line comment
|
|
35
|
+
if (char === "/" && nextChar === "/") {
|
|
36
|
+
while (i < text.length && text[i] !== "\n" && text[i] !== "\r") {
|
|
37
|
+
i++;
|
|
38
|
+
}
|
|
39
|
+
if (i < text.length) result += text[i];
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Block comment
|
|
44
|
+
if (char === "/" && nextChar === "*") {
|
|
45
|
+
i += 2;
|
|
46
|
+
while (i < text.length - 1 && !(text[i] === "*" && text[i + 1] === "/")) {
|
|
47
|
+
i++;
|
|
48
|
+
}
|
|
49
|
+
i++; // Skip closing '/'
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
result += char;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Remove trailing commas before } or ]
|
|
57
|
+
return result.replace(/,\s*([}\]])/g, "$1");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Safely parses a JSONC (JSON with comments and trailing commas) string.
|
|
62
|
+
*/
|
|
63
|
+
export function parseJsonc<T = unknown>(text: string): T {
|
|
64
|
+
const sanitized = stripJsonComments(text.trim());
|
|
65
|
+
return JSON.parse(sanitized) as T;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Safely parses a JSONC string without throwing errors.
|
|
70
|
+
* Returns the provided fallback value if parsing fails.
|
|
71
|
+
*/
|
|
72
|
+
export function tryParseJsonc<T>(text: string, fallback: T): T {
|
|
73
|
+
try {
|
|
74
|
+
return parseJsonc<T>(text);
|
|
75
|
+
} catch {
|
|
76
|
+
return fallback;
|
|
77
|
+
}
|
|
78
|
+
}
|