@nseng-ai/kernel 0.1.2
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/README.md +130 -0
- package/package.json +37 -0
- package/src/cli/completion.ts +30 -0
- package/src/cli/context.ts +110 -0
- package/src/cli/index.ts +668 -0
- package/src/cli/shell.ts +93 -0
- package/src/extensions/built-in-extension-commands.ts +306 -0
- package/src/extensions/command-registry.ts +342 -0
- package/src/extensions/discovery.ts +744 -0
- package/src/extensions/loader.ts +49 -0
- package/src/extensions/module-reference.ts +54 -0
- package/src/extensions/registry.ts +700 -0
- package/src/extensions/repo-local-catalog.ts +31 -0
- package/src/extensions/zod-issue-path.ts +65 -0
- package/src/project-config/points.ts +920 -0
- package/src/runtime/command-io.ts +149 -0
- package/src/runtime/diagnostics.ts +35 -0
- package/src/runtime/extension-root-discovery.ts +118 -0
- package/src/runtime/module-import.ts +4 -0
- package/src/runtime/module-loader.ts +75 -0
- package/src/runtime/pi-text-generation.ts +162 -0
- package/src/sdk/command-name.ts +2 -0
- package/src/sdk/command.ts +109 -0
- package/src/sdk/execution.ts +67 -0
- package/src/sdk/extension-manifest.ts +38 -0
- package/src/sdk/index.ts +73 -0
- package/src/sdk/progress-phase-state.ts +352 -0
- package/src/sdk/repo-local-ns-extension.ts +41 -0
- package/src/sdk/result.ts +11 -0
- package/src/sdk/schema.ts +3 -0
- package/src/sdk/services.ts +68 -0
- package/src/sdk/text-generation.ts +21 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { optionalEntries, optionalEntry } from "@nseng-ai/foundation/primitives";
|
|
2
|
+
import type {
|
|
3
|
+
NsCommandIo,
|
|
4
|
+
NsCommandMessageOptions,
|
|
5
|
+
NsExtensionApi,
|
|
6
|
+
NsNotifyLevel,
|
|
7
|
+
NsOutputStream,
|
|
8
|
+
} from "../sdk/index.ts";
|
|
9
|
+
|
|
10
|
+
export interface NsExtensionCommandIoOptions {
|
|
11
|
+
statusKey?: string;
|
|
12
|
+
shouldSuppress?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CliCommandIoInput {
|
|
16
|
+
stdout?: (text: string) => void;
|
|
17
|
+
stderr?: (text: string) => void;
|
|
18
|
+
onOutput?: (stream: NsOutputStream, text: string) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CliCommandIoOptions {
|
|
22
|
+
/** Invoked once per error-level notification, e.g. to flip a CLI exit flag. */
|
|
23
|
+
onNotifyError?: () => void;
|
|
24
|
+
/** Suppress transient phase and info notifications for structured output modes. */
|
|
25
|
+
shouldSuppress?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CommandIoChannels {
|
|
29
|
+
/** Preferred transient phase sink (e.g. live widget). */
|
|
30
|
+
phaseTransient?: (text: string) => void;
|
|
31
|
+
/** Sticky phase sink that must be cleared (e.g. Pi setStatus); receives undefined to clear. */
|
|
32
|
+
phaseSticky?: (value: string | undefined) => void;
|
|
33
|
+
/** Durable fallback for phase when no transient/sticky channel exists (e.g. stderr). */
|
|
34
|
+
phaseFallback?: (text: string) => void;
|
|
35
|
+
/** Notification sink for informational messages. */
|
|
36
|
+
notifyInfo?: (text: string) => void;
|
|
37
|
+
/** Notification sink for warning/error messages. */
|
|
38
|
+
notifyDiagnostic?: (text: string) => void;
|
|
39
|
+
/** Notification via Pi UI (level-aware), when present. */
|
|
40
|
+
notifyUi?: (message: string, level?: NsNotifyLevel) => void;
|
|
41
|
+
/**
|
|
42
|
+
* Rich scrollback sink that renders durable messages and can carry opaque
|
|
43
|
+
* structured presentation details (e.g. a Pi custom message). When absent,
|
|
44
|
+
* `message` falls back to transient phase text (or is dropped when isRichOnly).
|
|
45
|
+
*/
|
|
46
|
+
richMessage?: (message: string, options: { level: NsNotifyLevel; details?: unknown }) => void;
|
|
47
|
+
/** Suppress transient phase entirely (machine/structured output). */
|
|
48
|
+
shouldSuppress?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export { noopNsCommandIo, noopNsProgress } from "../sdk/index.ts";
|
|
52
|
+
|
|
53
|
+
export async function runWithNsCommandIo<T>(
|
|
54
|
+
io: NsCommandIo,
|
|
55
|
+
fn: (io: NsCommandIo) => Promise<T>,
|
|
56
|
+
): Promise<T> {
|
|
57
|
+
try {
|
|
58
|
+
return await fn(io);
|
|
59
|
+
} finally {
|
|
60
|
+
io.clearPhase();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function createCliCommandIo(
|
|
65
|
+
input: CliCommandIoInput,
|
|
66
|
+
options: CliCommandIoOptions = {},
|
|
67
|
+
): NsCommandIo {
|
|
68
|
+
const onOutput = input.onOutput;
|
|
69
|
+
const io = createCommandIo({
|
|
70
|
+
...optionalEntries({
|
|
71
|
+
phaseTransient:
|
|
72
|
+
onOutput === undefined ? undefined : (text: string) => onOutput("stderr", text),
|
|
73
|
+
phaseFallback: input.stderr,
|
|
74
|
+
notifyInfo: input.stdout,
|
|
75
|
+
notifyDiagnostic: input.stderr,
|
|
76
|
+
shouldSuppress: options.shouldSuppress,
|
|
77
|
+
}),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const onNotifyError = options.onNotifyError;
|
|
81
|
+
if (onNotifyError === undefined) return io;
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
...io,
|
|
85
|
+
notify: (message, level = "info") => {
|
|
86
|
+
if (level === "error") onNotifyError();
|
|
87
|
+
io.notify(message, level);
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function createCommandIo(channels: CommandIoChannels): NsCommandIo {
|
|
93
|
+
function phase(message: string): void {
|
|
94
|
+
if (channels.shouldSuppress === true) return;
|
|
95
|
+
if (channels.phaseSticky !== undefined) {
|
|
96
|
+
channels.phaseSticky(message);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const line = `${message}\n`;
|
|
100
|
+
if (channels.phaseTransient !== undefined) {
|
|
101
|
+
channels.phaseTransient(line);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
channels.phaseFallback?.(line);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function notify(message: string, level: NsNotifyLevel = "info"): void {
|
|
108
|
+
if (channels.notifyUi !== undefined) {
|
|
109
|
+
channels.notifyUi(message, level);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const line = `${message.trimEnd()}\n`;
|
|
113
|
+
if (level === "info" && channels.shouldSuppress !== true) {
|
|
114
|
+
channels.notifyInfo?.(line);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
channels.notifyDiagnostic?.(line);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function message(text: string, options: NsCommandMessageOptions = {}): void {
|
|
121
|
+
const level = options.level ?? "info";
|
|
122
|
+
if (channels.richMessage !== undefined) {
|
|
123
|
+
channels.richMessage(text, {
|
|
124
|
+
level,
|
|
125
|
+
...optionalEntry("details", options.details),
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (options.isRichOnly === true) return;
|
|
130
|
+
phase(text);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function clearPhase(): void {
|
|
134
|
+
channels.phaseSticky?.(undefined);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { phase, notify, message, clearPhase };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function commandIoFromNsExtensionApi(
|
|
141
|
+
ctx: NsExtensionApi,
|
|
142
|
+
options: NsExtensionCommandIoOptions = {},
|
|
143
|
+
): NsCommandIo {
|
|
144
|
+
if (options.shouldSuppress === undefined) return ctx.commandIo;
|
|
145
|
+
return createCliCommandIo(
|
|
146
|
+
optionalEntries({ stdout: ctx.stdout, stderr: ctx.stderr, onOutput: ctx.onOutput }),
|
|
147
|
+
{ shouldSuppress: options.shouldSuppress },
|
|
148
|
+
);
|
|
149
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { optionalEntry } from "@nseng-ai/foundation/primitives";
|
|
2
|
+
|
|
3
|
+
export type KernelDiagnosticSeverity = "error" | "info";
|
|
4
|
+
|
|
5
|
+
export interface KernelDiagnosticBase {
|
|
6
|
+
severity: KernelDiagnosticSeverity;
|
|
7
|
+
code: string;
|
|
8
|
+
message: string;
|
|
9
|
+
path?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface KernelDiagnosticRequest<TExtra extends object> {
|
|
13
|
+
code: string;
|
|
14
|
+
message: string;
|
|
15
|
+
path?: string;
|
|
16
|
+
extra?: TExtra;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function makeKernelDiagnostic<TExtra extends object = Record<never, never>>(
|
|
20
|
+
request: KernelDiagnosticRequest<TExtra>,
|
|
21
|
+
): (KernelDiagnosticBase & { severity: "error" }) & TExtra;
|
|
22
|
+
export function makeKernelDiagnostic<TExtra extends object = Record<never, never>>(
|
|
23
|
+
request: KernelDiagnosticRequest<TExtra> & { severity: "info" },
|
|
24
|
+
): (KernelDiagnosticBase & { severity: "info" }) & TExtra;
|
|
25
|
+
export function makeKernelDiagnostic<TExtra extends object = Record<never, never>>(
|
|
26
|
+
request: KernelDiagnosticRequest<TExtra> & { severity?: KernelDiagnosticSeverity },
|
|
27
|
+
): KernelDiagnosticBase & TExtra {
|
|
28
|
+
const base = {
|
|
29
|
+
severity: request.severity ?? "error",
|
|
30
|
+
code: request.code,
|
|
31
|
+
message: request.message,
|
|
32
|
+
...optionalEntry("path", request.path),
|
|
33
|
+
};
|
|
34
|
+
return { ...base, ...(request.extra ?? {}) } as KernelDiagnosticBase & TExtra;
|
|
35
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { formatErrorMessage, optionalEntry } from "@nseng-ai/foundation/primitives";
|
|
5
|
+
|
|
6
|
+
import { makeKernelDiagnostic } from "./diagnostics.ts";
|
|
7
|
+
|
|
8
|
+
export interface ExtensionRootScanDiagnostic {
|
|
9
|
+
severity: "error";
|
|
10
|
+
code: string;
|
|
11
|
+
message: string;
|
|
12
|
+
path?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type ExtensionRootEntry =
|
|
16
|
+
| { type: "file"; name: string; path: string }
|
|
17
|
+
| {
|
|
18
|
+
type: "directory";
|
|
19
|
+
name: string;
|
|
20
|
+
path: string;
|
|
21
|
+
packageJsonPath?: string;
|
|
22
|
+
indexPath?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export interface ExtensionRootScanResult {
|
|
26
|
+
entries: readonly ExtensionRootEntry[];
|
|
27
|
+
diagnostics: readonly ExtensionRootScanDiagnostic[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function scanExtensionRoot(rootDir: string): ExtensionRootScanResult {
|
|
31
|
+
if (!existsSync(rootDir)) return { entries: [], diagnostics: [] };
|
|
32
|
+
|
|
33
|
+
let isDirectory: boolean;
|
|
34
|
+
try {
|
|
35
|
+
isDirectory = statSync(rootDir).isDirectory();
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return {
|
|
38
|
+
entries: [],
|
|
39
|
+
diagnostics: [
|
|
40
|
+
diagnostic(
|
|
41
|
+
"extension_root_stat_failed",
|
|
42
|
+
`Could not inspect extension root ${rootDir}.\n${formatErrorMessage(error)}`,
|
|
43
|
+
{ path: rootDir },
|
|
44
|
+
),
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (!isDirectory) {
|
|
49
|
+
return {
|
|
50
|
+
entries: [],
|
|
51
|
+
diagnostics: [
|
|
52
|
+
diagnostic(
|
|
53
|
+
"extension_root_not_directory",
|
|
54
|
+
`Extension root must be a directory: ${rootDir}.`,
|
|
55
|
+
{
|
|
56
|
+
path: rootDir,
|
|
57
|
+
},
|
|
58
|
+
),
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let dirents;
|
|
64
|
+
try {
|
|
65
|
+
dirents = readdirSync(rootDir, { withFileTypes: true });
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return {
|
|
68
|
+
entries: [],
|
|
69
|
+
diagnostics: [
|
|
70
|
+
diagnostic(
|
|
71
|
+
"extension_root_read_failed",
|
|
72
|
+
`Could not read extension root ${rootDir}.\n${formatErrorMessage(error)}`,
|
|
73
|
+
{ path: rootDir },
|
|
74
|
+
),
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const entries: ExtensionRootEntry[] = [];
|
|
80
|
+
for (const dirent of dirents.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
81
|
+
const entryPath = join(rootDir, dirent.name);
|
|
82
|
+
if (dirent.isFile()) {
|
|
83
|
+
entries.push({ type: "file", name: dirent.name, path: entryPath });
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (!dirent.isDirectory()) continue;
|
|
87
|
+
|
|
88
|
+
const packageJsonPath = join(entryPath, "package.json");
|
|
89
|
+
if (existsSync(packageJsonPath)) {
|
|
90
|
+
entries.push({ type: "directory", name: dirent.name, path: entryPath, packageJsonPath });
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const indexPath = firstExistingDirectoryIndex(entryPath);
|
|
94
|
+
entries.push({
|
|
95
|
+
type: "directory",
|
|
96
|
+
name: dirent.name,
|
|
97
|
+
path: entryPath,
|
|
98
|
+
...(indexPath === undefined ? {} : { indexPath }),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return { entries, diagnostics: [] };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function firstExistingDirectoryIndex(entryPath: string): string | undefined {
|
|
105
|
+
for (const indexFileName of ["index.ts", "index.js"] as const) {
|
|
106
|
+
const indexPath = join(entryPath, indexFileName);
|
|
107
|
+
if (existsSync(indexPath)) return indexPath;
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function diagnostic(
|
|
113
|
+
code: string,
|
|
114
|
+
message: string,
|
|
115
|
+
options: { path?: string } = {},
|
|
116
|
+
): ExtensionRootScanDiagnostic {
|
|
117
|
+
return makeKernelDiagnostic({ code, message, ...optionalEntry("path", options.path) });
|
|
118
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { createJiti } from "jiti/static";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
defineExtension,
|
|
5
|
+
defineRepoLocalNsExtensionDescriptor,
|
|
6
|
+
failed,
|
|
7
|
+
noopNsCommandIo,
|
|
8
|
+
repoLocalNsCommandDescriptor,
|
|
9
|
+
noopNsProgress,
|
|
10
|
+
normalizeTextOutput,
|
|
11
|
+
ok,
|
|
12
|
+
nsExtensionManifestCommandSchema,
|
|
13
|
+
nsExtensionManifestSchema,
|
|
14
|
+
nsExtensionPackageManifestSchema,
|
|
15
|
+
stripOuterCodeFence,
|
|
16
|
+
trimOuterBlankLines,
|
|
17
|
+
truncateTextHead,
|
|
18
|
+
truncateTextHeadTail,
|
|
19
|
+
z,
|
|
20
|
+
} from "../sdk/index.ts";
|
|
21
|
+
|
|
22
|
+
/** Module specifier that ns command entries import the SDK from. */
|
|
23
|
+
const SDK_SPECIFIER = "@nseng-ai/kernel/sdk";
|
|
24
|
+
|
|
25
|
+
// Keep this object in sync with all runtime value exports from @nseng-ai/kernel/sdk; type-only exports are erased.
|
|
26
|
+
// Descriptor helpers are test-authoring-only today, but stay here while they are runtime exports.
|
|
27
|
+
const nsSdkVirtualModule = {
|
|
28
|
+
defineExtension,
|
|
29
|
+
defineRepoLocalNsExtensionDescriptor,
|
|
30
|
+
failed,
|
|
31
|
+
noopNsCommandIo,
|
|
32
|
+
repoLocalNsCommandDescriptor,
|
|
33
|
+
noopNsProgress,
|
|
34
|
+
normalizeTextOutput,
|
|
35
|
+
ok,
|
|
36
|
+
nsExtensionManifestCommandSchema,
|
|
37
|
+
nsExtensionManifestSchema,
|
|
38
|
+
nsExtensionPackageManifestSchema,
|
|
39
|
+
stripOuterCodeFence,
|
|
40
|
+
trimOuterBlankLines,
|
|
41
|
+
truncateTextHead,
|
|
42
|
+
truncateTextHeadTail,
|
|
43
|
+
z,
|
|
44
|
+
} satisfies Record<string, unknown>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create the ns-aware jiti instance used for user-authored modules.
|
|
48
|
+
*
|
|
49
|
+
* The load-bearing option is `virtualModules`: it binds `@nseng-ai/kernel/sdk` to the
|
|
50
|
+
* exact SDK object imported by this process, so command-entry commands and
|
|
51
|
+
* schemas share host SDK identity instead of resolving dependency copies from
|
|
52
|
+
* `.ns/extensions`.
|
|
53
|
+
*
|
|
54
|
+
* First-party bundled commands are loaded by package module specifier through
|
|
55
|
+
* the selected-command loader instead of through source-checkout aliases here.
|
|
56
|
+
*/
|
|
57
|
+
export function createNsJiti(): ReturnType<typeof createJiti> {
|
|
58
|
+
return createJiti(import.meta.url, {
|
|
59
|
+
moduleCache: false,
|
|
60
|
+
virtualModules: {
|
|
61
|
+
[SDK_SPECIFIER]: nsSdkVirtualModule,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Load the default export of a TypeScript or JavaScript ns user module.
|
|
68
|
+
*
|
|
69
|
+
* Callers validate the returned value according to the command-entry contract.
|
|
70
|
+
* Throws when the file cannot be transpiled or imported.
|
|
71
|
+
*/
|
|
72
|
+
export async function loadNsUserModuleDefault(modulePath: string): Promise<unknown> {
|
|
73
|
+
const jiti = createNsJiti();
|
|
74
|
+
return jiti.import(modulePath, { default: true });
|
|
75
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { Api, completeSimple, Model } from "@earendil-works/pi-ai";
|
|
2
|
+
|
|
3
|
+
import { formatErrorMessage } from "@nseng-ai/foundation/primitives";
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
TextGenerationRequest,
|
|
7
|
+
TextGenerationResult,
|
|
8
|
+
TextGenerationUsage,
|
|
9
|
+
TextGenerator,
|
|
10
|
+
} from "../sdk/index.ts";
|
|
11
|
+
|
|
12
|
+
const DEFAULT_MAX_TOKENS = 512;
|
|
13
|
+
const DEFAULT_REASONING = "low";
|
|
14
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
15
|
+
|
|
16
|
+
type CompleteSimpleFunction = typeof completeSimple;
|
|
17
|
+
|
|
18
|
+
type ModelAuth =
|
|
19
|
+
| { ok: true; apiKey?: string; headers?: Record<string, string> }
|
|
20
|
+
| { ok: false; error: string };
|
|
21
|
+
|
|
22
|
+
export interface PiModelRegistry {
|
|
23
|
+
find(provider: string, modelId: string): Model<Api> | undefined;
|
|
24
|
+
getApiKeyAndHeaders(model: Model<Api>): Promise<ModelAuth>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface PiTextGeneratorOptions {
|
|
28
|
+
modelRegistry?: PiModelRegistry;
|
|
29
|
+
completeSimple?: CompleteSimpleFunction;
|
|
30
|
+
loadDefaultModelRegistry?: () => Promise<PiModelRegistry>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class PiTextGenerator implements TextGenerator {
|
|
34
|
+
private readonly modelRegistry: PiModelRegistry | undefined;
|
|
35
|
+
private readonly completeSimple: CompleteSimpleFunction | undefined;
|
|
36
|
+
private readonly loadDefaultModelRegistry: () => Promise<PiModelRegistry>;
|
|
37
|
+
|
|
38
|
+
constructor(options: PiTextGeneratorOptions = {}) {
|
|
39
|
+
this.modelRegistry = options.modelRegistry;
|
|
40
|
+
this.completeSimple = options.completeSimple;
|
|
41
|
+
this.loadDefaultModelRegistry = options.loadDefaultModelRegistry ?? loadDefaultModelRegistry;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async generateText(request: TextGenerationRequest): Promise<TextGenerationResult> {
|
|
45
|
+
const parsed = parsePiModelRef(request.modelRef);
|
|
46
|
+
if (!parsed.ok) {
|
|
47
|
+
return { ok: false, error: parsed.error };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const modelRegistry = this.modelRegistry ?? (await this.loadDefaultModelRegistry());
|
|
51
|
+
const model = modelRegistry.find(parsed.provider, parsed.modelId);
|
|
52
|
+
if (model === undefined) {
|
|
53
|
+
return { ok: false, error: `Could not find Pi model ${request.modelRef}.` };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const auth = await modelRegistry.getApiKeyAndHeaders(model);
|
|
57
|
+
if (!auth.ok) {
|
|
58
|
+
return { ok: false, error: `Pi auth failed for ${request.modelRef}: ${auth.error}` };
|
|
59
|
+
}
|
|
60
|
+
if (!auth.apiKey) {
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
error: `No Pi auth found for ${parsed.provider}. Run /login or configure Pi auth.`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const completeSimple = this.completeSimple ?? (await loadCompleteSimple());
|
|
69
|
+
const response = await completeSimple(
|
|
70
|
+
model,
|
|
71
|
+
{
|
|
72
|
+
systemPrompt: request.system,
|
|
73
|
+
messages: [
|
|
74
|
+
{
|
|
75
|
+
role: "user",
|
|
76
|
+
content: [{ type: "text", text: request.prompt }],
|
|
77
|
+
timestamp: Date.now(),
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
...(auth.headers === undefined ? {} : { headers: auth.headers }),
|
|
83
|
+
apiKey: auth.apiKey,
|
|
84
|
+
maxTokens: request.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
85
|
+
reasoning: request.reasoning ?? DEFAULT_REASONING,
|
|
86
|
+
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (response.stopReason === "error" || response.stopReason === "aborted") {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
error: `Pi model ${request.modelRef} failed to generate text: ${response.errorMessage ?? response.stopReason}`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const text = response.content
|
|
98
|
+
.filter(
|
|
99
|
+
(content): content is { type: "text"; text: string } =>
|
|
100
|
+
content.type === "text" && typeof content.text === "string",
|
|
101
|
+
)
|
|
102
|
+
.map((content) => content.text)
|
|
103
|
+
.join("\n");
|
|
104
|
+
if (text.trim().length === 0) {
|
|
105
|
+
return { ok: false, error: `Pi model ${request.modelRef} returned empty text.` };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const usage = textGenerationUsageFromResponse(response.usage);
|
|
109
|
+
return {
|
|
110
|
+
ok: true,
|
|
111
|
+
text,
|
|
112
|
+
...(usage === undefined ? {} : { usage }),
|
|
113
|
+
};
|
|
114
|
+
} catch (error) {
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
error: `Pi model ${request.modelRef} failed to generate text: ${formatErrorMessage(error)}`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function textGenerationUsageFromResponse(usage: {
|
|
124
|
+
input: number;
|
|
125
|
+
output: number;
|
|
126
|
+
}): TextGenerationUsage | undefined {
|
|
127
|
+
if (!Number.isFinite(usage.input) || !Number.isFinite(usage.output)) return undefined;
|
|
128
|
+
return {
|
|
129
|
+
inputTokens: usage.input,
|
|
130
|
+
outputTokens: usage.output,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
type ParsedPiModelRef =
|
|
135
|
+
| { ok: true; provider: string; modelId: string }
|
|
136
|
+
| { ok: false; error: string };
|
|
137
|
+
|
|
138
|
+
function parsePiModelRef(modelRef: string): ParsedPiModelRef {
|
|
139
|
+
const separator = modelRef.indexOf("/");
|
|
140
|
+
if (separator <= 0 || separator === modelRef.length - 1) {
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
error: `Invalid Pi model reference ${JSON.stringify(modelRef)}. Expected provider/model-id.`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
ok: true,
|
|
149
|
+
provider: modelRef.slice(0, separator),
|
|
150
|
+
modelId: modelRef.slice(separator + 1),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function loadCompleteSimple(): Promise<CompleteSimpleFunction> {
|
|
155
|
+
const piAi = await import("@earendil-works/pi-ai");
|
|
156
|
+
return piAi.completeSimple;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function loadDefaultModelRegistry(): Promise<PiModelRegistry> {
|
|
160
|
+
const { AuthStorage, ModelRegistry } = await import("@earendil-works/pi-coding-agent");
|
|
161
|
+
return ModelRegistry.create(AuthStorage.create());
|
|
162
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ClinkrCompletionCandidate,
|
|
3
|
+
ClinkrCompletionResult,
|
|
4
|
+
ClinkrDynamicCompletionRequest,
|
|
5
|
+
ClinkrExit,
|
|
6
|
+
OptionSpec,
|
|
7
|
+
RenderCapabilities,
|
|
8
|
+
} from "@nseng-ai/clinkr";
|
|
9
|
+
import type { PositionalSpec } from "@nseng-ai/clinkr/raw";
|
|
10
|
+
import type { ExplicitUndefined } from "@nseng-ai/foundation/primitives";
|
|
11
|
+
import type { z } from "zod";
|
|
12
|
+
|
|
13
|
+
import type { NsExtensionApi } from "./execution.ts";
|
|
14
|
+
import type { NsResult } from "./result.ts";
|
|
15
|
+
|
|
16
|
+
export type {
|
|
17
|
+
ClinkrCompletionCandidate,
|
|
18
|
+
ClinkrCompletionResult,
|
|
19
|
+
ClinkrDynamicCompletionRequest,
|
|
20
|
+
ClinkrExit,
|
|
21
|
+
ClinkrFormat,
|
|
22
|
+
OptionSpec,
|
|
23
|
+
PositionalSpec,
|
|
24
|
+
RenderCapabilities,
|
|
25
|
+
} from "@nseng-ai/clinkr";
|
|
26
|
+
|
|
27
|
+
export type NsCommandSchema = z.ZodObject;
|
|
28
|
+
export type NsCommandRequest<S extends NsCommandSchema> = z.output<S>;
|
|
29
|
+
export type NsCommandCompletionProvider = (
|
|
30
|
+
ctx: NsExtensionApi,
|
|
31
|
+
request: ClinkrDynamicCompletionRequest,
|
|
32
|
+
) =>
|
|
33
|
+
| Promise<ClinkrCompletionResult | readonly ClinkrCompletionCandidate[]>
|
|
34
|
+
| ClinkrCompletionResult
|
|
35
|
+
| readonly ClinkrCompletionCandidate[];
|
|
36
|
+
|
|
37
|
+
export interface NsCommand<S extends NsCommandSchema = z.ZodObject, T = unknown> {
|
|
38
|
+
name: string;
|
|
39
|
+
summary: string;
|
|
40
|
+
description: string;
|
|
41
|
+
schema?: ExplicitUndefined<"public-api-compatibility", S>;
|
|
42
|
+
positionals?: ExplicitUndefined<
|
|
43
|
+
"public-api-compatibility",
|
|
44
|
+
Partial<Record<keyof z.infer<S> & string, PositionalSpec>>
|
|
45
|
+
>;
|
|
46
|
+
options?: ExplicitUndefined<
|
|
47
|
+
"public-api-compatibility",
|
|
48
|
+
Partial<Record<keyof z.infer<S> & string, OptionSpec>>
|
|
49
|
+
>;
|
|
50
|
+
resultSchema?: ExplicitUndefined<"public-api-compatibility", z.ZodType<T>>;
|
|
51
|
+
renderHuman?: ExplicitUndefined<
|
|
52
|
+
"public-api-compatibility",
|
|
53
|
+
(data: unknown, caps: RenderCapabilities) => string
|
|
54
|
+
>;
|
|
55
|
+
renderMarkdown?: ExplicitUndefined<
|
|
56
|
+
"public-api-compatibility",
|
|
57
|
+
(data: unknown, caps: RenderCapabilities) => string
|
|
58
|
+
>;
|
|
59
|
+
completionProvider?: ExplicitUndefined<"public-api-compatibility", NsCommandCompletionProvider>;
|
|
60
|
+
run(
|
|
61
|
+
ctx: NsExtensionApi,
|
|
62
|
+
request: z.output<S>,
|
|
63
|
+
): Promise<NsResult | ClinkrExit<T>> | NsResult | ClinkrExit<T>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface NsExtension<TCommands extends readonly NsCommand[] = readonly NsCommand[]> {
|
|
67
|
+
commands?: ExplicitUndefined<"overload-selector", TCommands>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type NsCommandTuple<TSchemas extends readonly NsCommandSchema[]> = {
|
|
71
|
+
readonly [Index in keyof TSchemas]: NsCommand<TSchemas[Index]>;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export function defineExtension(extension: {
|
|
75
|
+
commands?: ExplicitUndefined<"overload-selector", never>;
|
|
76
|
+
}): NsExtension<readonly []>;
|
|
77
|
+
export function defineExtension(extension: NsExtension<readonly []>): NsExtension<readonly []>;
|
|
78
|
+
export function defineExtension<S1 extends NsCommandSchema = z.ZodObject>(
|
|
79
|
+
extension: NsExtension<readonly [NsCommand<S1>]>,
|
|
80
|
+
): NsExtension<readonly [NsCommand<S1>]>;
|
|
81
|
+
export function defineExtension<
|
|
82
|
+
S1 extends NsCommandSchema = z.ZodObject,
|
|
83
|
+
S2 extends NsCommandSchema = z.ZodObject,
|
|
84
|
+
>(
|
|
85
|
+
extension: NsExtension<readonly [NsCommand<S1>, NsCommand<S2>]>,
|
|
86
|
+
): NsExtension<readonly [NsCommand<S1>, NsCommand<S2>]>;
|
|
87
|
+
export function defineExtension<
|
|
88
|
+
S1 extends NsCommandSchema = z.ZodObject,
|
|
89
|
+
S2 extends NsCommandSchema = z.ZodObject,
|
|
90
|
+
S3 extends NsCommandSchema = z.ZodObject,
|
|
91
|
+
>(
|
|
92
|
+
extension: NsExtension<readonly [NsCommand<S1>, NsCommand<S2>, NsCommand<S3>]>,
|
|
93
|
+
): NsExtension<readonly [NsCommand<S1>, NsCommand<S2>, NsCommand<S3>]>;
|
|
94
|
+
export function defineExtension<
|
|
95
|
+
S1 extends NsCommandSchema = z.ZodObject,
|
|
96
|
+
S2 extends NsCommandSchema = z.ZodObject,
|
|
97
|
+
S3 extends NsCommandSchema = z.ZodObject,
|
|
98
|
+
S4 extends NsCommandSchema = z.ZodObject,
|
|
99
|
+
const SRest extends readonly NsCommandSchema[] = readonly [],
|
|
100
|
+
>(
|
|
101
|
+
extension: NsExtension<
|
|
102
|
+
readonly [NsCommand<S1>, NsCommand<S2>, NsCommand<S3>, NsCommand<S4>, ...NsCommandTuple<SRest>]
|
|
103
|
+
>,
|
|
104
|
+
): NsExtension<
|
|
105
|
+
readonly [NsCommand<S1>, NsCommand<S2>, NsCommand<S3>, NsCommand<S4>, ...NsCommandTuple<SRest>]
|
|
106
|
+
>;
|
|
107
|
+
export function defineExtension(extension: NsExtension): NsExtension {
|
|
108
|
+
return extension;
|
|
109
|
+
}
|