pi-spark 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 +86 -0
- package/assets/cover.png +0 -0
- package/assets/screenshot.png +0 -0
- package/extensions/editor/config.ts +7 -0
- package/extensions/editor/index.ts +111 -0
- package/extensions/editor/spinner.ts +92 -0
- package/extensions/footer/config.ts +3 -0
- package/extensions/footer/index.ts +122 -0
- package/extensions/fullscreen/config.ts +3 -0
- package/extensions/fullscreen/filler.ts +36 -0
- package/extensions/fullscreen/index.ts +47 -0
- package/extensions/presets/config.ts +8 -0
- package/extensions/presets/index.ts +72 -0
- package/extensions/presets/manager.ts +113 -0
- package/extensions/presets/selector.ts +55 -0
- package/extensions/recap/config.ts +10 -0
- package/extensions/recap/idle.ts +106 -0
- package/extensions/recap/index.ts +72 -0
- package/extensions/recap/manager.ts +127 -0
- package/extensions/recap/model.ts +61 -0
- package/extensions/recap/widget.ts +42 -0
- package/extensions/shared/components/inline-text.ts +56 -0
- package/extensions/shared/components/split-line.ts +76 -0
- package/extensions/shared/config/index.ts +70 -0
- package/extensions/shared/config/model.ts +15 -0
- package/extensions/shared/config/schema.ts +18 -0
- package/extensions/shared/events/auto-collect-events.ts +40 -0
- package/extensions/shared/events/index.ts +3 -0
- package/extensions/shared/events/preset.ts +9 -0
- package/extensions/shared/format/index.ts +35 -0
- package/extensions/shared/usage/index.ts +79 -0
- package/package.json +53 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
import { InlineText } from "./inline-text";
|
|
4
|
+
|
|
5
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
6
|
+
|
|
7
|
+
type Side = "left" | "right";
|
|
8
|
+
|
|
9
|
+
interface SplitLineOptions {
|
|
10
|
+
padding?: number;
|
|
11
|
+
gap?: number;
|
|
12
|
+
innerPadding?: number;
|
|
13
|
+
primarySide?: Side;
|
|
14
|
+
spacingChar?: string;
|
|
15
|
+
ellipsis?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const DEFAULT_SPLIT_LINE_OPTIONS: Required<SplitLineOptions> = {
|
|
19
|
+
padding: 0,
|
|
20
|
+
gap: 2,
|
|
21
|
+
innerPadding: 0,
|
|
22
|
+
primarySide: "left",
|
|
23
|
+
spacingChar: " ",
|
|
24
|
+
ellipsis: "…",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Single-line component that places left and right text at opposite sides. */
|
|
28
|
+
export class SplitLine implements Component {
|
|
29
|
+
private left: InlineText;
|
|
30
|
+
private right: InlineText;
|
|
31
|
+
|
|
32
|
+
private padding: number;
|
|
33
|
+
private gap: number;
|
|
34
|
+
private primarySide: Side;
|
|
35
|
+
private spacingChar: string;
|
|
36
|
+
|
|
37
|
+
constructor(left: InlineText | string, right: InlineText | string, options: SplitLineOptions = {}) {
|
|
38
|
+
const spacingChar = options.spacingChar ?? DEFAULT_SPLIT_LINE_OPTIONS.spacingChar;
|
|
39
|
+
if (visibleWidth(spacingChar) !== 1) throw new Error("spacingChar must have a visible width of 1");
|
|
40
|
+
|
|
41
|
+
const inlineTextOptions = { padding: options.innerPadding ?? DEFAULT_SPLIT_LINE_OPTIONS.innerPadding, ellipsis: options.ellipsis ?? DEFAULT_SPLIT_LINE_OPTIONS.ellipsis };
|
|
42
|
+
this.left = typeof left === "string" ? new InlineText(left, inlineTextOptions) : left;
|
|
43
|
+
this.right = typeof right === "string" ? new InlineText(right, inlineTextOptions) : right;
|
|
44
|
+
|
|
45
|
+
this.padding = options.padding ?? DEFAULT_SPLIT_LINE_OPTIONS.padding;
|
|
46
|
+
this.gap = options.gap ?? DEFAULT_SPLIT_LINE_OPTIONS.gap;
|
|
47
|
+
this.primarySide = options.primarySide ?? DEFAULT_SPLIT_LINE_OPTIONS.primarySide;
|
|
48
|
+
this.spacingChar = spacingChar;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
invalidate(): void {
|
|
52
|
+
// No-op
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
render(width: number): [string] {
|
|
56
|
+
const contentWidth = width - this.padding * 2;
|
|
57
|
+
const content = this.renderContent(contentWidth);
|
|
58
|
+
const paddingText = this.spacingChar.repeat(this.padding);
|
|
59
|
+
|
|
60
|
+
return [`${paddingText}${content}${paddingText}`];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private renderContent(width: number): string {
|
|
64
|
+
const [primary, secondary] = this.primarySide === "left" ? [this.left, this.right] : [this.right, this.left];
|
|
65
|
+
|
|
66
|
+
const renderedPrimary = primary.render(width)[0];
|
|
67
|
+
const renderedPrimaryWidth = visibleWidth(renderedPrimary);
|
|
68
|
+
const secondaryWidth = renderedPrimaryWidth > 0 ? width - renderedPrimaryWidth - this.gap : width;
|
|
69
|
+
const renderedSecondary = secondary.render(secondaryWidth)[0];
|
|
70
|
+
|
|
71
|
+
const [renderedLeft, renderedRight] = this.primarySide === "left" ? [renderedPrimary, renderedSecondary] : [renderedSecondary, renderedPrimary];
|
|
72
|
+
const spacingWidth = width - visibleWidth(renderedLeft) - visibleWidth(renderedRight);
|
|
73
|
+
|
|
74
|
+
return `${renderedLeft}${this.spacingChar.repeat(spacingWidth)}${renderedRight}`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
import { configSchemas } from "./schema";
|
|
6
|
+
|
|
7
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import type { ConfigField, ConfigValue } from "./schema";
|
|
9
|
+
|
|
10
|
+
type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
|
|
11
|
+
type JsonObject = { [key: string]: JsonValue };
|
|
12
|
+
|
|
13
|
+
export function loadConfig<Field extends ConfigField>(ctx: ExtensionContext, field: Field, fileName: string = "spark.json"): ConfigValue<Field> | undefined {
|
|
14
|
+
const rawConfig = loadMergedJson(getConfigPaths(ctx.cwd, fileName));
|
|
15
|
+
const rawValue = rawConfig?.[field];
|
|
16
|
+
if (rawValue === false) return undefined;
|
|
17
|
+
|
|
18
|
+
const result = configSchemas[field].safeParse(rawValue === true || rawValue === undefined ? {} : rawValue);
|
|
19
|
+
if (result.success) return result.data as ConfigValue<Field>;
|
|
20
|
+
|
|
21
|
+
const message = result.error.issues.map((issue) => `${[field, ...issue.path].join(".")}: ${issue.message}`).join("; ");
|
|
22
|
+
ctx.ui.notify(`Invalid spark config: ${message}`, "error");
|
|
23
|
+
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getConfigPaths(cwd: string, fileName: string): [globalPath: string, projectPath: string] {
|
|
28
|
+
return [join(getAgentDir(), fileName), join(cwd, ".pi", fileName)];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function loadMergedJson(paths: string[]): JsonObject | undefined {
|
|
32
|
+
let merged: JsonObject | undefined;
|
|
33
|
+
paths.forEach((path) => {
|
|
34
|
+
const value = readJsonFile(path);
|
|
35
|
+
if (value === undefined) return;
|
|
36
|
+
|
|
37
|
+
merged = mergeConfig(merged, value);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return merged;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readJsonFile(path: string): JsonObject | undefined {
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(readFileSync(path, "utf8")) as JsonObject;
|
|
46
|
+
} catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function mergeConfig(base: JsonObject | undefined, override: JsonObject): JsonObject {
|
|
52
|
+
if (base === undefined) return override;
|
|
53
|
+
if (!isPlainObject(base) || !isPlainObject(override)) return override;
|
|
54
|
+
|
|
55
|
+
const result: Record<string, JsonValue> = { ...base };
|
|
56
|
+
Object.entries(override).forEach(([key, overrideValue]) => {
|
|
57
|
+
const baseValue = base[key];
|
|
58
|
+
if (isPlainObject(baseValue) && isPlainObject(overrideValue)) {
|
|
59
|
+
result[key] = { ...baseValue, ...overrideValue };
|
|
60
|
+
} else {
|
|
61
|
+
result[key] = overrideValue;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isPlainObject(value: unknown): value is Record<string, JsonValue> {
|
|
69
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
70
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
|
|
3
|
+
import type { ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
4
|
+
|
|
5
|
+
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const satisfies readonly ModelThinkingLevel[];
|
|
6
|
+
|
|
7
|
+
export const modelSchema = z.object({
|
|
8
|
+
provider: z.string().min(1),
|
|
9
|
+
model: z.string().min(1),
|
|
10
|
+
thinkingLevel: z.enum(THINKING_LEVELS),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const optionalModelSchema = modelSchema.partial();
|
|
14
|
+
|
|
15
|
+
export type OptionalModelConfig = z.infer<typeof optionalModelSchema>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
|
|
3
|
+
import { editorConfigSchema } from "../../editor/config";
|
|
4
|
+
import { footerConfigSchema } from "../../footer/config";
|
|
5
|
+
import { fullscreenConfigSchema } from "../../fullscreen/config";
|
|
6
|
+
import { recapConfigSchema } from "../../recap/config";
|
|
7
|
+
import { presetsConfigSchema } from "../../presets/config";
|
|
8
|
+
|
|
9
|
+
export const configSchemas = {
|
|
10
|
+
editor: editorConfigSchema,
|
|
11
|
+
footer: footerConfigSchema,
|
|
12
|
+
fullscreen: fullscreenConfigSchema,
|
|
13
|
+
recap: recapConfigSchema,
|
|
14
|
+
presets: presetsConfigSchema,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type ConfigField = keyof typeof configSchemas;
|
|
18
|
+
export type ConfigValue<Field extends ConfigField> = z.infer<(typeof configSchemas)[Field]>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ExtensionAPI, EventBus } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
class EventCollector {
|
|
4
|
+
private pi: ExtensionAPI;
|
|
5
|
+
private collected: Set<() => void> = new Set();
|
|
6
|
+
|
|
7
|
+
constructor(pi: ExtensionAPI) {
|
|
8
|
+
this.pi = pi;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
on(...args: Parameters<EventBus["on"]>): ReturnType<EventBus["on"]> {
|
|
12
|
+
const unsubscribe = this.pi.events.on(...args);
|
|
13
|
+
this.collected.add(unsubscribe);
|
|
14
|
+
|
|
15
|
+
return this.wrap(unsubscribe);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
dispose(): void {
|
|
19
|
+
this.collected.forEach((unsubscribe) => unsubscribe());
|
|
20
|
+
this.collected.clear();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private wrap(unsubscribe: () => void): () => void {
|
|
24
|
+
return () => {
|
|
25
|
+
if (this.collected.delete(unsubscribe)) {
|
|
26
|
+
unsubscribe();
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function autoCollectEvents(pi: ExtensionAPI): EventCollector {
|
|
33
|
+
const collector = new EventCollector(pi);
|
|
34
|
+
|
|
35
|
+
pi.on("session_shutdown", () => {
|
|
36
|
+
collector.dispose();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return collector;
|
|
40
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
|
|
3
|
+
const presetChangePayloadSchema = z.string().min(1).optional();
|
|
4
|
+
|
|
5
|
+
export const PRESET_CHANGE = "preset:change" as const;
|
|
6
|
+
|
|
7
|
+
export function parsePresetChange(data: unknown): z.infer<typeof presetChangePayloadSchema> {
|
|
8
|
+
return presetChangePayloadSchema.parse(data);
|
|
9
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Provider } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ContextUsage } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
|
|
4
|
+
export function formatModel(provider?: Provider, model?: string, thinkingLevel?: string): string {
|
|
5
|
+
return provider && model ? `${provider}/${model}${thinkingLevel ? `:${thinkingLevel}` : ""}` : "no-model";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function formatTokens(count: number): string {
|
|
9
|
+
if (count < 1_000) return count.toString();
|
|
10
|
+
if (count < 10_000) return `${(count / 1_000).toFixed(1)}K`;
|
|
11
|
+
if (count < 1_000_000) return `${Math.round(count / 1_000)}K`;
|
|
12
|
+
if (count < 10_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
|
|
13
|
+
return `${Math.round(count / 1_000_000)}M`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function formatContextUsage(contextUsage: ContextUsage | undefined): string {
|
|
17
|
+
const tokens = contextUsage?.tokens ?? null;
|
|
18
|
+
const contextWindow = contextUsage?.contextWindow ?? null;
|
|
19
|
+
const percent = contextUsage?.percent ?? null;
|
|
20
|
+
const percentText = percent === null ? "?" : `${percent.toFixed(1)}%`;
|
|
21
|
+
|
|
22
|
+
return `${tokens === null ? "?" : formatTokens(tokens)}/${contextWindow === null ? "?" : formatTokens(contextWindow)} (${percentText})`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function formatCost(cost: number, isSubscription: boolean): string {
|
|
26
|
+
return `$${cost.toFixed(2)}${isSubscription ? " (sub)" : ""}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Replace newlines, tabs, carriage returns with space, then collapse multiple spaces */
|
|
30
|
+
export function sanitizeText(text: string): string {
|
|
31
|
+
return text
|
|
32
|
+
.replace(/[\r\n\t]/g, " ")
|
|
33
|
+
.replace(/ +/g, " ")
|
|
34
|
+
.trim();
|
|
35
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Usage } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
|
|
4
|
+
/** Structural type guard for the pi `Usage` shape. */
|
|
5
|
+
export function isUsage(value: unknown): value is Usage {
|
|
6
|
+
if (typeof value !== "object" || value === null) return false;
|
|
7
|
+
|
|
8
|
+
const usage = value as Record<string, unknown>;
|
|
9
|
+
const hasTokenFields =
|
|
10
|
+
typeof usage.input === "number" &&
|
|
11
|
+
typeof usage.output === "number" &&
|
|
12
|
+
typeof usage.cacheRead === "number" &&
|
|
13
|
+
typeof usage.cacheWrite === "number" &&
|
|
14
|
+
typeof usage.totalTokens === "number";
|
|
15
|
+
if (!hasTokenFields) return false;
|
|
16
|
+
|
|
17
|
+
if (typeof usage.cost !== "object" || usage.cost === null) return false;
|
|
18
|
+
const cost = usage.cost as Record<string, unknown>;
|
|
19
|
+
return (
|
|
20
|
+
typeof cost.input === "number" &&
|
|
21
|
+
typeof cost.output === "number" &&
|
|
22
|
+
typeof cost.cacheRead === "number" &&
|
|
23
|
+
typeof cost.cacheWrite === "number" &&
|
|
24
|
+
typeof cost.total === "number"
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Sum two `Usage` values into a new one. */
|
|
29
|
+
export function addUsage(a: Usage, b: Usage): Usage {
|
|
30
|
+
return {
|
|
31
|
+
input: a.input + b.input,
|
|
32
|
+
output: a.output + b.output,
|
|
33
|
+
cacheRead: a.cacheRead + b.cacheRead,
|
|
34
|
+
cacheWrite: a.cacheWrite + b.cacheWrite,
|
|
35
|
+
totalTokens: a.totalTokens + b.totalTokens,
|
|
36
|
+
cost: {
|
|
37
|
+
input: a.cost.input + b.cost.input,
|
|
38
|
+
output: a.cost.output + b.cost.output,
|
|
39
|
+
cacheRead: a.cost.cacheRead + b.cost.cacheRead,
|
|
40
|
+
cacheWrite: a.cost.cacheWrite + b.cost.cacheWrite,
|
|
41
|
+
total: a.cost.total + b.cost.total,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Extract usage from a session entry, classifying it as subscription or paid.
|
|
48
|
+
*
|
|
49
|
+
* `usage`/`provider`/`model` live in different fields depending on the entry type:
|
|
50
|
+
*
|
|
51
|
+
* - `message` carries them on `.message`
|
|
52
|
+
* - `custom` on `.data`
|
|
53
|
+
* - `custom_message` on `.details`.
|
|
54
|
+
*
|
|
55
|
+
* Other entry types carry no usage. Returns `undefined` when the resolved field has no `usage`.
|
|
56
|
+
*
|
|
57
|
+
* An entry counts as subscription only when its `provider`/`model` resolve to an OAuth model in the
|
|
58
|
+
* registry; everything else (including entries without `provider`/`model`) is treated as paid.
|
|
59
|
+
*/
|
|
60
|
+
export function getEntryUsage(ctx: ExtensionContext, entry: SessionEntry): { type: "subscription" | "paid"; usage: Usage } | undefined {
|
|
61
|
+
let source: unknown;
|
|
62
|
+
if (entry.type === "message") source = entry.message;
|
|
63
|
+
else if (entry.type === "custom") source = entry.data;
|
|
64
|
+
else if (entry.type === "custom_message") source = entry.details;
|
|
65
|
+
else return;
|
|
66
|
+
|
|
67
|
+
if (typeof source !== "object" || source === null) return;
|
|
68
|
+
const data = source as { usage?: unknown; provider?: unknown; model?: unknown };
|
|
69
|
+
if (!isUsage(data.usage)) return;
|
|
70
|
+
|
|
71
|
+
const type = isSubscription(ctx, data.provider, data.model) ? "subscription" : "paid";
|
|
72
|
+
return { type, usage: data.usage };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isSubscription(ctx: ExtensionContext, provider: unknown, model: unknown): boolean {
|
|
76
|
+
if (typeof provider !== "string" || typeof model !== "string") return false;
|
|
77
|
+
const resolved = ctx.modelRegistry.find(provider, model);
|
|
78
|
+
return resolved ? ctx.modelRegistry.isUsingOAuth(resolved) : false;
|
|
79
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-spark",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A small, opinionated collection of pi extensions",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-coding-agent",
|
|
7
|
+
"pi-package"
|
|
8
|
+
],
|
|
9
|
+
"homepage": "https://github.com/zlliang/pi-spark#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/zlliang/pi-spark/issues"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/zlliang/pi-spark.git"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"type": "module",
|
|
19
|
+
"files": [
|
|
20
|
+
"extensions",
|
|
21
|
+
"assets",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"pi": {
|
|
26
|
+
"extensions": ["./extensions"],
|
|
27
|
+
"image": "https://raw.githubusercontent.com/zlliang/pi-spark/main/assets/cover.png"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"start": "pi --no-extensions --extension ~/.pi/agent --extension $PWD",
|
|
31
|
+
"typecheck": "tsc --noEmit",
|
|
32
|
+
"prepublishOnly": "npm run typecheck"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"zod": "^4.4.3"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@earendil-works/pi-agent-core": "*",
|
|
39
|
+
"@earendil-works/pi-ai": "*",
|
|
40
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
41
|
+
"@earendil-works/pi-tui": "*",
|
|
42
|
+
"typebox": "*"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@earendil-works/pi-agent-core": "*",
|
|
46
|
+
"@earendil-works/pi-ai": "*",
|
|
47
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
48
|
+
"@earendil-works/pi-tui": "*",
|
|
49
|
+
"@types/node": "*",
|
|
50
|
+
"typebox": "*",
|
|
51
|
+
"typescript": "^6.0.3"
|
|
52
|
+
}
|
|
53
|
+
}
|