@jancellor/ask 1.0.0 → 1.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.
Files changed (101) hide show
  1. package/README.md +87 -7
  2. package/dist/agent/agent.d.ts +38 -0
  3. package/dist/agent/agent.js +20 -18
  4. package/dist/agent/agents-prompt.d.ts +4 -0
  5. package/dist/agent/check.d.ts +1 -0
  6. package/dist/agent/check.js +5 -0
  7. package/dist/agent/config-schema.d.ts +56 -0
  8. package/dist/agent/config-schema.js +26 -0
  9. package/dist/agent/config-store.d.ts +12 -0
  10. package/dist/agent/config-store.js +46 -0
  11. package/dist/agent/config.d.ts +27 -0
  12. package/dist/agent/config.js +84 -46
  13. package/dist/agent/execute-tool.d.ts +15 -0
  14. package/dist/agent/execute-tool.js +1 -3
  15. package/dist/agent/fs-errors.js +23 -0
  16. package/dist/agent/fs-ops.d.ts +2 -0
  17. package/dist/agent/fs-ops.js +14 -0
  18. package/dist/agent/index.d.ts +1 -0
  19. package/dist/agent/init-prompt.d.ts +3 -0
  20. package/dist/agent/messages.d.ts +10 -0
  21. package/dist/agent/openai-subscription-fetch.d.ts +5 -0
  22. package/dist/agent/openai-subscription-fetch.js +34 -34
  23. package/dist/agent/paths.d.ts +1 -0
  24. package/dist/agent/provider-factories.d.ts +9 -0
  25. package/dist/agent/provider-factories.js +21 -0
  26. package/dist/agent/serializer.d.ts +6 -0
  27. package/dist/agent/serializer.js +4 -6
  28. package/dist/agent/session-store.d.ts +18 -0
  29. package/dist/agent/session-store.js +4 -12
  30. package/dist/agent/session.d.ts +18 -0
  31. package/dist/agent/session.js +1 -1
  32. package/dist/agent/skills-prompt.d.ts +6 -0
  33. package/dist/agent/system-prompt.d.ts +3 -0
  34. package/dist/agent/tools.d.ts +6 -0
  35. package/dist/batch/index.d.ts +4 -0
  36. package/dist/batch/index.js +1 -1
  37. package/dist/batch/run.d.ts +4 -0
  38. package/dist/batch/run.js +101 -0
  39. package/dist/cli.d.ts +2 -0
  40. package/dist/cli.js +56 -0
  41. package/dist/config/index.d.ts +6 -0
  42. package/dist/config/index.js +22 -0
  43. package/dist/config/run.d.ts +6 -0
  44. package/dist/config/run.js +22 -0
  45. package/dist/index.d.ts +2 -0
  46. package/dist/index.js +2 -32
  47. package/dist/interactive/app.d.ts +7 -0
  48. package/dist/interactive/app.js +12 -0
  49. package/dist/interactive/assistant-part-message.d.ts +4 -0
  50. package/dist/interactive/assistant-part-message.js +10 -0
  51. package/dist/interactive/execute-tool-part-message.d.ts +2 -0
  52. package/dist/interactive/execute-tool-part-message.js +48 -0
  53. package/dist/interactive/generic-tool-part-message.d.ts +2 -0
  54. package/dist/interactive/generic-tool-part-message.js +28 -0
  55. package/dist/interactive/input.d.ts +8 -0
  56. package/dist/interactive/input.js +115 -0
  57. package/dist/interactive/markdown.d.ts +1 -0
  58. package/dist/interactive/markdown.js +31 -0
  59. package/dist/interactive/messages.d.ts +9 -0
  60. package/dist/interactive/messages.js +87 -0
  61. package/dist/interactive/run.d.ts +4 -0
  62. package/dist/interactive/run.js +18 -0
  63. package/dist/interactive/spinner-message.d.ts +1 -0
  64. package/dist/interactive/spinner-message.js +14 -0
  65. package/dist/interactive/tool-part-message.d.ts +12 -0
  66. package/dist/interactive/tool-part-message.js +9 -0
  67. package/dist/interactive/use-agent.d.ts +13 -0
  68. package/dist/interactive/use-agent.js +27 -0
  69. package/dist/interactive/use-input-state.d.ts +19 -0
  70. package/dist/interactive/use-input-state.js +136 -0
  71. package/dist/interactive/user-part-message.d.ts +3 -0
  72. package/dist/interactive/user-part-message.js +9 -0
  73. package/dist/interactive/welcome.d.ts +7 -0
  74. package/dist/interactive/welcome.js +12 -0
  75. package/dist/shutdown-manager.d.ts +8 -0
  76. package/dist/tui/app.d.ts +7 -0
  77. package/dist/tui/app.js +2 -2
  78. package/dist/tui/assistant-part-message.d.ts +4 -0
  79. package/dist/tui/assistant-part-message.js +2 -2
  80. package/dist/tui/execute-tool-part-message.d.ts +2 -0
  81. package/dist/tui/execute-tool-part-message.js +1 -1
  82. package/dist/tui/generic-tool-part-message.d.ts +2 -0
  83. package/dist/tui/generic-tool-part-message.js +1 -1
  84. package/dist/tui/index.d.ts +4 -0
  85. package/dist/tui/input.d.ts +8 -0
  86. package/dist/tui/input.js +1 -1
  87. package/dist/tui/markdown.d.ts +1 -0
  88. package/dist/tui/messages.d.ts +9 -0
  89. package/dist/tui/messages.js +4 -3
  90. package/dist/tui/run.d.ts +4 -0
  91. package/dist/tui/run.js +18 -0
  92. package/dist/tui/spinner-message.d.ts +1 -0
  93. package/dist/tui/tool-part-message.d.ts +12 -0
  94. package/dist/tui/tool-part-message.js +3 -3
  95. package/dist/tui/use-agent.d.ts +13 -0
  96. package/dist/tui/use-agent.js +3 -3
  97. package/dist/tui/use-input-state.d.ts +19 -0
  98. package/dist/tui/user-part-message.d.ts +3 -0
  99. package/dist/tui/welcome.d.ts +7 -0
  100. package/dist/tui/welcome.js +3 -3
  101. package/package.json +17 -5
package/README.md CHANGED
@@ -50,24 +50,104 @@ npm link
50
50
  Configure a provider:
51
51
 
52
52
  ```bash
53
- export ASK_API_KEY="your-api-key"
54
- export ASK_MODEL="anthropic/claude-sonnet-4.6"
55
- export ASK_BASE_URL="https://openrouter.ai/api/v1"
53
+ ask --config --provider anthropic --model claude-opus-4-6
56
54
  ```
57
55
 
58
- Or create `~/.config/ask/config.json`:
56
+ That saves the current config and creates `~/.config/ask/config.json`, eg:
59
57
 
60
58
  ```json
61
59
  {
62
- "api_key": "your-api-key",
63
- "model": "anthropic/claude-sonnet-4.6",
64
- "base_url": "https://openrouter.ai/api/v1"
60
+ "currentProvider": "anthropic",
61
+ "providers": {
62
+ "anthropic": {
63
+ "currentModel": "claude-opus-4-6"
64
+ }
65
+ }
65
66
  }
66
67
  ```
67
68
 
69
+ Store secrets separately in `~/.config/ask/config.secrets.json`:
70
+
71
+ ```json
72
+ {
73
+ "anthropic": {
74
+ "apiKey": "your-api-key"
75
+ }
76
+ }
77
+ ```
78
+
79
+ For OpenAI-compatible endpoints, configure the provider explicitly:
80
+
81
+ ```json
82
+ {
83
+ "currentProvider": "openrouter",
84
+ "providers": {
85
+ "openrouter": {
86
+ "sdkProvider": "openai-compatible",
87
+ "providerOptions": {
88
+ "name": "openrouter",
89
+ "baseURL": "https://openrouter.ai/api/v1"
90
+ },
91
+ "currentModel": "anthropic/claude-sonnet-4.6"
92
+ }
93
+ }
94
+ }
95
+ ```
96
+
97
+ Ask uses the [AI SDK](https://ai-sdk.dev/docs/reference/ai-sdk-core), and this
98
+ config is designed to map directly onto that runtime model. You select a
99
+ configured provider, then a model within that provider, and optionally a
100
+ variant within that model. A configured provider can also override
101
+ [`sdkProvider`](https://ai-sdk.dev/docs/providers) so one named config entry can
102
+ target a different SDK provider family, such as `openai-compatible`.
103
+ `providerOptions` live on the provider and are passed to the SDK provider
104
+ factory, while auth stays separate in `config.secrets.json`. `generateOptions`
105
+ can be set globally and at the provider, model, and variant levels; at runtime
106
+ they are merged in that order and passed through as the options object for
107
+ [`generateText`](https://ai-sdk.dev/docs/ai-sdk-core/generating-text).
108
+
109
+ For example, you can add a Claude reasoning-effort variant with per-variant
110
+ Anthropic options:
111
+
112
+ ```json
113
+ {
114
+ "currentProvider": "anthropic",
115
+ "providers": {
116
+ "anthropic": {
117
+ "currentModel": "claude-opus-4-6",
118
+ "models": {
119
+ "claude-opus-4-6": {
120
+ "currentVariant": "balanced",
121
+ "variants": {
122
+ "balanced": {
123
+ "generateOptions": {
124
+ "providerOptions": {
125
+ "anthropic": {
126
+ "effort": "medium"
127
+ }
128
+ }
129
+ }
130
+ }
131
+ }
132
+ }
133
+ }
134
+ }
135
+ }
136
+ }
137
+ ```
138
+
139
+ Then use `ask -v balanced` to select that variant for a run.
140
+
141
+ Use `ask -c` to print the resolved config. If you pass `-p`, `-m`, or `-v`
142
+ with `-c`, those values are saved as the new current selection. Without `-c`,
143
+ they apply only to the current run.
144
+
68
145
  Run:
69
146
 
70
147
  ```bash
148
+ ask -c # Show current resolved config
149
+ ask -c -p openai -m gpt-5 # Update saved provider/model
150
+ ask -c -v # Clear the saved variant
71
151
  ask # Interactive mode
72
152
  ask "refactor" # Batch mode (single positional arg)
73
153
  cat file.ts | ask "explain" # Pipe context, ask a question
@@ -0,0 +1,38 @@
1
+ import { type ConfigOptions } from './config.js';
2
+ import type { AskMessage } from './messages.js';
3
+ import { type SessionOptions } from './session.js';
4
+ export type { AskMessage, AskMessageMeta } from './messages.js';
5
+ export interface AgentListener {
6
+ onMessages?(messages: AskMessage[]): void | Promise<void>;
7
+ onClear?(): void | Promise<void>;
8
+ onFork?(): void | Promise<void>;
9
+ }
10
+ export type AgentOptions = SessionOptions & ConfigOptions;
11
+ export declare const ABORTED_MESSAGE = "[Aborted]";
12
+ export declare const ERROR_MESSAGE = "[Error]";
13
+ export declare class Agent {
14
+ private listeners;
15
+ private config;
16
+ private systemPrompt;
17
+ private tools;
18
+ private session;
19
+ private serializer;
20
+ private controller;
21
+ private constructor();
22
+ static create(options: AgentOptions): Promise<Agent>;
23
+ addListener(listener: AgentListener): void;
24
+ removeListener(listener: AgentListener): void;
25
+ get messages(): AskMessage[];
26
+ get sessionId(): string;
27
+ get model(): string;
28
+ get provider(): string;
29
+ get variant(): string | null;
30
+ ask(message: string): Promise<void>;
31
+ abort(): void;
32
+ cancelAll(): Promise<void>;
33
+ clear(beforeClear?: () => void): Promise<void>;
34
+ fork(sessionId?: string, beforeFork?: () => void): Promise<void>;
35
+ private addMessages;
36
+ private addInitialMessages;
37
+ private callTools;
38
+ }
@@ -1,6 +1,5 @@
1
- import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
2
1
  import { generateText, } from 'ai';
3
- import { ConfigReader } from './config.js';
2
+ import { ConfigReader, } from './config.js';
4
3
  import { InitPrompt } from './init-prompt.js';
5
4
  import { Session } from './session.js';
6
5
  import { SystemPrompt } from './system-prompt.js';
@@ -9,32 +8,25 @@ import { Tools } from './tools.js';
9
8
  export const ABORTED_MESSAGE = '[Aborted]';
10
9
  export const ERROR_MESSAGE = '[Error]';
11
10
  export class Agent {
12
- modelId;
13
- baseUrl;
14
11
  listeners = [];
15
- languageModel;
12
+ config;
16
13
  systemPrompt;
17
14
  tools;
18
15
  session;
19
16
  serializer = new Serializer();
20
17
  controller = null;
21
- constructor(session) {
18
+ constructor(session, config) {
22
19
  this.session = session;
23
- const config = new ConfigReader().read();
24
- this.modelId = config.model;
25
- this.baseUrl = config.baseUrl;
26
- const provider = createOpenAICompatible({
27
- name: 'ask',
28
- apiKey: config.apiKey,
29
- baseURL: config.baseUrl,
30
- });
31
- this.languageModel = provider(config.model);
20
+ this.config = config;
32
21
  this.systemPrompt = new SystemPrompt().build();
33
22
  this.tools = new Tools();
34
23
  }
35
24
  static async create(options) {
36
- const session = await Session.create(options);
37
- return new Agent(session);
25
+ const [session, config] = await Promise.all([
26
+ Session.create(options),
27
+ new ConfigReader().resolve(options),
28
+ ]);
29
+ return new Agent(session, config);
38
30
  }
39
31
  addListener(listener) {
40
32
  this.listeners.push(listener);
@@ -50,6 +42,15 @@ export class Agent {
50
42
  get sessionId() {
51
43
  return this.session.sessionId;
52
44
  }
45
+ get model() {
46
+ return this.config.model;
47
+ }
48
+ get provider() {
49
+ return this.config.provider;
50
+ }
51
+ get variant() {
52
+ return this.config.variant;
53
+ }
53
54
  ask(message) {
54
55
  return this.serializer.submit(async () => {
55
56
  await this.addInitialMessages();
@@ -59,7 +60,8 @@ export class Agent {
59
60
  try {
60
61
  while (true) {
61
62
  const result = await generateText({
62
- model: this.languageModel,
63
+ ...this.config.generateOptions,
64
+ model: this.config.languageModel,
63
65
  system: this.systemPrompt,
64
66
  messages: this.session.messages,
65
67
  tools: this.tools.definitions(),
@@ -0,0 +1,4 @@
1
+ export declare class AgentsPrompt {
2
+ build(): Promise<string>;
3
+ private tryRead;
4
+ }
@@ -0,0 +1 @@
1
+ export declare function check(condition: unknown, message: string): asserts condition;
@@ -0,0 +1,5 @@
1
+ export function check(condition, message) {
2
+ if (!condition) {
3
+ throw new Error(message);
4
+ }
5
+ }
@@ -0,0 +1,56 @@
1
+ import { z } from 'zod';
2
+ export declare const GenerateOptions: z.ZodRecord<z.ZodString, z.ZodUnknown>;
3
+ export declare const ProviderOptions: z.ZodRecord<z.ZodString, z.ZodUnknown>;
4
+ export declare const ProviderSecretOptions: z.ZodRecord<z.ZodString, z.ZodUnknown>;
5
+ export declare const VariantConfig: z.ZodObject<{
6
+ generateOptions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
7
+ }, z.core.$strip>;
8
+ export declare const ModelConfig: z.ZodObject<{
9
+ sdkModel: z.ZodOptional<z.ZodString>;
10
+ generateOptions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
11
+ currentVariant: z.ZodOptional<z.ZodNullable<z.ZodString>>;
12
+ variants: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
13
+ generateOptions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
14
+ }, z.core.$strip>>>;
15
+ }, z.core.$strip>;
16
+ export declare const ProviderConfig: z.ZodObject<{
17
+ sdkProvider: z.ZodOptional<z.ZodString>;
18
+ providerOptions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
19
+ generateOptions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
20
+ currentModel: z.ZodOptional<z.ZodString>;
21
+ models: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
22
+ sdkModel: z.ZodOptional<z.ZodString>;
23
+ generateOptions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
24
+ currentVariant: z.ZodOptional<z.ZodNullable<z.ZodString>>;
25
+ variants: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
26
+ generateOptions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
27
+ }, z.core.$strip>>>;
28
+ }, z.core.$strip>>>;
29
+ }, z.core.$strip>;
30
+ export declare const Config: z.ZodObject<{
31
+ currentProvider: z.ZodOptional<z.ZodString>;
32
+ generateOptions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
33
+ providers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
34
+ sdkProvider: z.ZodOptional<z.ZodString>;
35
+ providerOptions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
36
+ generateOptions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
37
+ currentModel: z.ZodOptional<z.ZodString>;
38
+ models: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
39
+ sdkModel: z.ZodOptional<z.ZodString>;
40
+ generateOptions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
41
+ currentVariant: z.ZodOptional<z.ZodNullable<z.ZodString>>;
42
+ variants: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
43
+ generateOptions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
44
+ }, z.core.$strip>>>;
45
+ }, z.core.$strip>>>;
46
+ }, z.core.$strip>>>;
47
+ }, z.core.$strip>;
48
+ export declare const ConfigSecrets: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>;
49
+ export type GenerateOptions = z.infer<typeof GenerateOptions>;
50
+ export type ProviderOptions = z.infer<typeof ProviderOptions>;
51
+ export type ProviderSecretOptions = z.infer<typeof ProviderSecretOptions>;
52
+ export type Config = z.infer<typeof Config>;
53
+ export type ProviderConfig = z.infer<typeof ProviderConfig>;
54
+ export type ModelConfig = z.infer<typeof ModelConfig>;
55
+ export type VariantConfig = z.infer<typeof VariantConfig>;
56
+ export type ConfigSecrets = z.infer<typeof ConfigSecrets>;
@@ -0,0 +1,26 @@
1
+ import { z } from 'zod';
2
+ export const GenerateOptions = z.record(z.string(), z.unknown());
3
+ export const ProviderOptions = z.record(z.string(), z.unknown());
4
+ export const ProviderSecretOptions = z.record(z.string(), z.unknown());
5
+ export const VariantConfig = z.object({
6
+ generateOptions: GenerateOptions.optional(),
7
+ });
8
+ export const ModelConfig = z.object({
9
+ sdkModel: z.string().optional(),
10
+ generateOptions: GenerateOptions.optional(),
11
+ currentVariant: z.string().nullable().optional(),
12
+ variants: z.record(z.string(), VariantConfig).optional(),
13
+ });
14
+ export const ProviderConfig = z.object({
15
+ sdkProvider: z.string().optional(),
16
+ providerOptions: ProviderOptions.optional(),
17
+ generateOptions: GenerateOptions.optional(),
18
+ currentModel: z.string().optional(),
19
+ models: z.record(z.string(), ModelConfig).optional(),
20
+ });
21
+ export const Config = z.object({
22
+ currentProvider: z.string().optional(),
23
+ generateOptions: GenerateOptions.optional(),
24
+ providers: z.record(z.string(), ProviderConfig).optional(),
25
+ });
26
+ export const ConfigSecrets = z.record(z.string(), ProviderSecretOptions);
@@ -0,0 +1,12 @@
1
+ import { ConfigSecrets, type Config as ConfigType } from './config-schema.js';
2
+ export declare class ConfigStore {
3
+ private static CONFIG_DIR;
4
+ private static CONFIG_PATH;
5
+ private static SECRETS_PATH;
6
+ readConfig(): Promise<ConfigType>;
7
+ readSecrets(): Promise<ConfigSecrets>;
8
+ writeConfig(config: ConfigType): Promise<void>;
9
+ private readJsonFile;
10
+ private parseWithSchema;
11
+ private writeConfigAtomically;
12
+ }
@@ -0,0 +1,46 @@
1
+ import { mkdir, readFile, rename, writeFile } from 'fs/promises';
2
+ import { homedir } from 'os';
3
+ import { join } from 'path';
4
+ import { Config, ConfigSecrets, } from './config-schema.js';
5
+ import { ignoreMissing } from './fs-ops.js';
6
+ export class ConfigStore {
7
+ static CONFIG_DIR = join(homedir(), '.config', 'ask');
8
+ static CONFIG_PATH = join(ConfigStore.CONFIG_DIR, 'config.json');
9
+ static SECRETS_PATH = join(ConfigStore.CONFIG_DIR, 'config.secrets.json');
10
+ async readConfig() {
11
+ const raw = await this.readJsonFile(ConfigStore.CONFIG_PATH);
12
+ return this.parseWithSchema('config.json', Config, raw ?? {});
13
+ }
14
+ async readSecrets() {
15
+ const raw = await this.readJsonFile(ConfigStore.SECRETS_PATH);
16
+ return this.parseWithSchema('config.secrets.json', ConfigSecrets, raw ?? {});
17
+ }
18
+ async writeConfig(config) {
19
+ const serialized = JSON.stringify(config, null, 2) + '\n';
20
+ await this.writeConfigAtomically(serialized);
21
+ }
22
+ async readJsonFile(path) {
23
+ const content = await ignoreMissing(() => readFile(path, 'utf-8'));
24
+ if (content === undefined)
25
+ return undefined;
26
+ try {
27
+ return JSON.parse(content);
28
+ }
29
+ catch {
30
+ throw new Error(`invalid JSON in ${path}`);
31
+ }
32
+ }
33
+ parseWithSchema(schemaName, parser, input) {
34
+ const result = parser.safeParse(input);
35
+ if (result.success)
36
+ return result.data;
37
+ throw new Error(`invalid ${schemaName}: ${JSON.stringify(result.error.issues, null, 2)}`);
38
+ }
39
+ async writeConfigAtomically(data) {
40
+ await mkdir(ConfigStore.CONFIG_DIR, { recursive: true });
41
+ const path = ConfigStore.CONFIG_PATH;
42
+ const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
43
+ await writeFile(tempPath, data, 'utf-8');
44
+ await rename(tempPath, path);
45
+ }
46
+ }
@@ -0,0 +1,27 @@
1
+ import type { LanguageModel } from 'ai';
2
+ import { type GenerateOptions, type ProviderOptions } from './config-schema.js';
3
+ export type ConfigOptions = {
4
+ provider?: string;
5
+ model?: string;
6
+ variant?: string | null;
7
+ saveAsCurrent?: boolean;
8
+ };
9
+ export type ResolvedConfig = {
10
+ provider: string;
11
+ model: string;
12
+ variant: string | null;
13
+ sdkProvider: string;
14
+ sdkModel: string;
15
+ providerOptions: ProviderOptions;
16
+ generateOptions: GenerateOptions;
17
+ languageModel: LanguageModel;
18
+ };
19
+ export declare class ConfigReader {
20
+ private store;
21
+ constructor();
22
+ resolve(configOptions: ConfigOptions): Promise<ResolvedConfig>;
23
+ private saveAsCurrent;
24
+ private resolveProvider;
25
+ private resolveModel;
26
+ private resolveVariant;
27
+ }
@@ -1,54 +1,92 @@
1
- import { readFileSync } from 'fs';
2
- import { homedir } from 'os';
3
- import { join } from 'path';
1
+ import { isMatch, merge } from 'lodash-es';
2
+ import { check } from './check.js';
3
+ import { ConfigStore } from './config-store.js';
4
+ import { createLanguageModel } from './provider-factories.js';
4
5
  export class ConfigReader {
5
- static CONFIG_PATH = join(homedir(), '.config', 'ask', 'config.json');
6
- readConfigFile() {
7
- try {
8
- return JSON.parse(readFileSync(ConfigReader.CONFIG_PATH, 'utf-8'));
9
- }
10
- catch {
11
- return {};
12
- }
13
- }
14
- envOrFile(envKey, fileKey, fileValues) {
15
- const envValue = process.env[envKey];
16
- if (envValue)
17
- return envValue.trim();
18
- const fileValue = fileValues[fileKey];
19
- if (fileValue != null)
20
- return String(fileValue).trim();
21
- throw new Error(`neither ${envKey} nor ${fileKey} is set`);
22
- }
23
- optionalEnvOrFile(envKey, fileKey, fileValues) {
24
- const envValue = process.env[envKey];
25
- if (envValue && envValue.trim())
26
- return envValue.trim();
27
- const fileValue = fileValues[fileKey];
28
- if (fileValue == null)
29
- return undefined;
30
- const value = String(fileValue).trim();
31
- return value ? value : undefined;
6
+ store;
7
+ constructor() {
8
+ this.store = new ConfigStore();
32
9
  }
33
- isOpenAIBaseUrl(baseUrl) {
34
- try {
35
- return new URL(baseUrl).hostname.includes('openai.com');
36
- }
37
- catch {
38
- return baseUrl.includes('openai.com');
10
+ async resolve(configOptions) {
11
+ const [config, secrets] = await Promise.all([
12
+ this.store.readConfig(),
13
+ this.store.readSecrets(),
14
+ ]);
15
+ const { provider, providerConfig } = this.resolveProvider(config, configOptions);
16
+ const { model, modelConfig } = this.resolveModel(providerConfig, configOptions);
17
+ const { variant, variantConfig } = this.resolveVariant(modelConfig, configOptions);
18
+ const sdkProvider = providerConfig.sdkProvider ?? provider;
19
+ const sdkModel = modelConfig.sdkModel ?? model;
20
+ const providerOptions = providerConfig.providerOptions ?? {};
21
+ const providerSecretOptions = secrets[provider] ?? {};
22
+ if (configOptions.saveAsCurrent) {
23
+ await this.saveAsCurrent(config, provider, model, variant);
39
24
  }
25
+ const generateOptions = merge({}, config.generateOptions, providerConfig.generateOptions, modelConfig.generateOptions, variantConfig?.generateOptions);
26
+ const languageModel = createLanguageModel({
27
+ sdkProvider,
28
+ sdkModel,
29
+ providerOptions,
30
+ providerSecretOptions,
31
+ });
32
+ return {
33
+ provider,
34
+ model,
35
+ variant,
36
+ sdkProvider,
37
+ sdkModel,
38
+ providerOptions,
39
+ generateOptions,
40
+ languageModel,
41
+ };
40
42
  }
41
- read() {
42
- const fileValues = this.readConfigFile();
43
- const baseUrl = this.envOrFile('ASK_BASE_URL', 'base_url', fileValues);
44
- const apiKey = this.optionalEnvOrFile('ASK_API_KEY', 'api_key', fileValues);
45
- if (!apiKey && !this.isOpenAIBaseUrl(baseUrl)) {
46
- throw new Error('neither ASK_API_KEY nor api_key is set (required unless ASK_BASE_URL/base_url points to openai.com)');
43
+ async saveAsCurrent(config, provider, model, variant) {
44
+ const persistedVariant = config.providers?.[provider]?.models?.[model]?.currentVariant ?? null;
45
+ // prevent unnecessary config only to set `currentVariant: null`
46
+ const shouldOmitNullVariant = variant === null && persistedVariant === null;
47
+ const patch = {
48
+ currentProvider: provider,
49
+ providers: {
50
+ [provider]: {
51
+ currentModel: model,
52
+ ...(!shouldOmitNullVariant
53
+ ? {
54
+ models: {
55
+ [model]: {
56
+ currentVariant: variant,
57
+ },
58
+ },
59
+ }
60
+ : {}),
61
+ },
62
+ },
63
+ };
64
+ if (!isMatch(config, patch)) {
65
+ const patched = merge({}, config, patch);
66
+ await this.store.writeConfig(patched);
47
67
  }
48
- return {
49
- apiKey: apiKey ?? 'oauth',
50
- model: this.envOrFile('ASK_MODEL', 'model', fileValues),
51
- baseUrl,
68
+ }
69
+ resolveProvider(config, configOptions) {
70
+ const provider = configOptions.provider ?? config.currentProvider;
71
+ check(provider, 'provider not specified');
72
+ const providerConfig = config.providers?.[provider] ?? {
73
+ sdkProvider: provider,
52
74
  };
75
+ return { provider, providerConfig };
76
+ }
77
+ resolveModel(provider, configOptions) {
78
+ const model = configOptions.model ?? provider.currentModel;
79
+ check(model, `model not specified`);
80
+ const modelConfig = provider.models?.[model] ?? {
81
+ sdkModel: model,
82
+ };
83
+ return { model, modelConfig };
84
+ }
85
+ resolveVariant(model, configOptions) {
86
+ const variant = configOptions.variant ?? model.currentVariant ?? null;
87
+ const variantConfig = variant ? model.variants?.[variant] : undefined;
88
+ if (variant)
89
+ check(variantConfig, `variant not found: ${variant}`);
90
+ return { variant, variantConfig };
53
91
  }
54
92
  }
@@ -0,0 +1,15 @@
1
+ export type ExecuteToolOutput = {
2
+ exit?: number;
3
+ signal?: string;
4
+ stdout?: string;
5
+ stderr?: string;
6
+ error?: string;
7
+ };
8
+ export declare class ExecuteTool {
9
+ readonly name = "execute";
10
+ definition(): import("ai").Tool<{
11
+ command: string;
12
+ }, never>;
13
+ execute(input: unknown, signal: AbortSignal): Promise<ExecuteToolOutput>;
14
+ private signalProcessGroup;
15
+ }
@@ -5,9 +5,7 @@ import { z } from 'zod';
5
5
  const DEFAULT_TIMEOUT_S = 60;
6
6
  const TERMINATION_GRACE_MS = 5000;
7
7
  const executeInputSchema = z.object({
8
- command: z
9
- .string()
10
- .describe('The shell command to execute using `bash -c`'),
8
+ command: z.string().describe('The shell command to execute using `bash -c`'),
11
9
  });
12
10
  export class ExecuteTool {
13
11
  name = 'execute';
@@ -0,0 +1,23 @@
1
+ export function isEnoentError(error) {
2
+ if (!error || typeof error !== 'object')
3
+ return false;
4
+ return 'code' in error && error.code === 'ENOENT';
5
+ }
6
+ export async function ignoreMissing(op) {
7
+ try {
8
+ return await op();
9
+ }
10
+ catch (error) {
11
+ if (!isEnoentError(error))
12
+ throw error;
13
+ }
14
+ }
15
+ export function ignoreMissingSync(op) {
16
+ try {
17
+ return op();
18
+ }
19
+ catch (error) {
20
+ if (!isEnoentError(error))
21
+ throw error;
22
+ }
23
+ }
@@ -0,0 +1,2 @@
1
+ export declare function isEnoentError(error: unknown): boolean;
2
+ export declare function ignoreMissing<T>(op: () => Promise<T>): Promise<T | undefined>;
@@ -0,0 +1,14 @@
1
+ export function isEnoentError(error) {
2
+ if (!error || typeof error !== 'object')
3
+ return false;
4
+ return 'code' in error && error.code === 'ENOENT';
5
+ }
6
+ export async function ignoreMissing(op) {
7
+ try {
8
+ return await op();
9
+ }
10
+ catch (error) {
11
+ if (!isEnoentError(error))
12
+ throw error;
13
+ }
14
+ }
@@ -0,0 +1 @@
1
+ export { Agent, ABORTED_MESSAGE, ERROR_MESSAGE, type AskMessage, type AgentOptions, type AskMessageMeta, type AgentListener, } from './agent.js';
@@ -0,0 +1,3 @@
1
+ export declare class InitPrompt {
2
+ build(): Promise<string>;
3
+ }
@@ -0,0 +1,10 @@
1
+ import type { ModelMessage } from 'ai';
2
+ export type AskMessageMeta = {
3
+ id: string;
4
+ parentId: string | null;
5
+ uiHidden?: boolean;
6
+ timestamp?: string;
7
+ };
8
+ export type AskMessage = ModelMessage & {
9
+ _meta: AskMessageMeta;
10
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * I don't think this works but leaving it plugged in for now.
3
+ * We might need to set other headers/content in order to use subscriptions.
4
+ */
5
+ export declare function createOpenAISubscriptionFetch(): (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;