octo-dev 0.7.1 → 0.8.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/package.json +5 -2
- package/src/cli/config.command.ts +49 -2
- package/src/cli/index.ts +9 -1
- package/src/infra/compose-smart-merger.ts +1 -1
- package/src/shared/config.ts +50 -0
- package/src/shared/git-auth.ts +4 -4
- package/src/shared/llm.ts +57 -46
- package/src/shared/prompt.ts +41 -0
- package/src/shared/sync.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "octo-dev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Build orchestration, semantic versioning, and local infrastructure management for repository workspaces",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,9 +43,12 @@
|
|
|
43
43
|
"type-check": "tsc --noEmit"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
+
"ai": "^6.0.194",
|
|
47
|
+
"@ai-sdk/openai": "^3.0.67",
|
|
48
|
+
"@ai-sdk/google": "^3.0.80",
|
|
49
|
+
"@ai-sdk/anthropic": "^3.0.81",
|
|
46
50
|
"commander": "^13.1.0",
|
|
47
51
|
"execa": "^9.6.1",
|
|
48
|
-
"@huggingface/transformers": "^4.2.0",
|
|
49
52
|
"pino": "^10.3.1",
|
|
50
53
|
"pino-pretty": "^13.1.3",
|
|
51
54
|
"semver": "^7.7.2",
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import { run } from '../shared/process-runner.js';
|
|
2
2
|
import { logger } from '../shared/logger.js';
|
|
3
|
+
import { saveConfig, getConfigPath, type LlmConfig } from '../shared/config.js';
|
|
4
|
+
import { ask, askSecret } from '../shared/prompt.js';
|
|
5
|
+
|
|
6
|
+
const PROVIDERS: Record<string, { baseUrl: string; defaultModel: string }> = {
|
|
7
|
+
openai: { baseUrl: 'https://openai.com/v1', defaultModel: 'gpt-4o-mini' },
|
|
8
|
+
gemini: { baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai', defaultModel: 'gemini-2.0-flash' },
|
|
9
|
+
groq: { baseUrl: 'https://api.groq.com/openai/v1', defaultModel: 'llama-3.3-70b-versatile' },
|
|
10
|
+
anthropic: { baseUrl: 'https://api.anthropic.com/v1', defaultModel: 'claude-sonnet-4-20250514' },
|
|
11
|
+
custom: { baseUrl: '', defaultModel: '' },
|
|
12
|
+
};
|
|
3
13
|
|
|
4
14
|
/**
|
|
5
|
-
* octo config git-cache
|
|
15
|
+
* octo config git-cache [value]
|
|
16
|
+
*
|
|
17
|
+
* Sets git credential.helper locally.
|
|
6
18
|
*
|
|
7
|
-
*
|
|
19
|
+
* @param value - The credential helper value (default: "cache").
|
|
8
20
|
*/
|
|
9
21
|
export async function configGitCacheCommand(value?: string): Promise<void> {
|
|
10
22
|
const helper = value || 'cache';
|
|
@@ -15,3 +27,38 @@ export async function configGitCacheCommand(value?: string): Promise<void> {
|
|
|
15
27
|
}
|
|
16
28
|
logger.info(`Git credential.helper set to "${helper}".`);
|
|
17
29
|
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* octo config llm
|
|
33
|
+
*
|
|
34
|
+
* Configures the LLM provider interactively.
|
|
35
|
+
* Stores api key and provider settings in ~/.octo-config.json.
|
|
36
|
+
*/
|
|
37
|
+
export async function configLlmCommand(): Promise<void> {
|
|
38
|
+
const providerNames = Object.keys(PROVIDERS);
|
|
39
|
+
logger.info(`Available providers: ${providerNames.join(', ')}`);
|
|
40
|
+
|
|
41
|
+
const provider = await ask('Provider: ');
|
|
42
|
+
if (!providerNames.includes(provider)) {
|
|
43
|
+
logger.error(`Unknown provider "${provider}". Available: ${providerNames.join(', ')}`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const preset = PROVIDERS[provider];
|
|
48
|
+
const baseUrl = provider === 'custom'
|
|
49
|
+
? await ask('Base URL (OpenAI-compatible): ')
|
|
50
|
+
: preset.baseUrl;
|
|
51
|
+
|
|
52
|
+
const model = await ask(`Model [${preset.defaultModel}]: `) || preset.defaultModel;
|
|
53
|
+
const apiKey = await askSecret('API key: ');
|
|
54
|
+
|
|
55
|
+
if (!apiKey) {
|
|
56
|
+
logger.error('API key is required.');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const llm: LlmConfig = { provider, baseUrl, model, apiKey };
|
|
61
|
+
saveConfig({ llm });
|
|
62
|
+
|
|
63
|
+
logger.info(`LLM configured (${provider}/${model}). Saved to ${getConfigPath()}`);
|
|
64
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ const program = new Command();
|
|
|
11
11
|
program
|
|
12
12
|
.name('octo')
|
|
13
13
|
.description('Build orchestration, semantic versioning, and local infrastructure management for repository workspaces')
|
|
14
|
-
.version('0.
|
|
14
|
+
.version('0.8.0');
|
|
15
15
|
|
|
16
16
|
program
|
|
17
17
|
.command('init')
|
|
@@ -106,4 +106,12 @@ config
|
|
|
106
106
|
await configGitCacheCommand(value);
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
+
config
|
|
110
|
+
.command('llm')
|
|
111
|
+
.description('Configure LLM provider (openai, gemini, groq, anthropic, custom)')
|
|
112
|
+
.action(async () => {
|
|
113
|
+
const { configLlmCommand } = await import('./config.command.js');
|
|
114
|
+
await configLlmCommand();
|
|
115
|
+
});
|
|
116
|
+
|
|
109
117
|
program.parse();
|
|
@@ -46,7 +46,7 @@ export function createComposeSmartMerger(): ComposeSmartMerger {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
// Try LLM-based merge
|
|
49
|
-
if (
|
|
49
|
+
if (isAvailable()) {
|
|
50
50
|
try {
|
|
51
51
|
logger.info('Using local AI for smart compose merge...');
|
|
52
52
|
const prompt = buildPrompt(composes);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
|
|
5
|
+
const CONFIG_FILE = join(homedir(), '.octo-config.json');
|
|
6
|
+
|
|
7
|
+
export interface LlmConfig {
|
|
8
|
+
provider: string;
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
model: string;
|
|
11
|
+
apiKey: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface OctoConfig {
|
|
15
|
+
llm?: LlmConfig;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Loads the octo configuration from ~/.octo-config.json.
|
|
20
|
+
* Returns an empty config object if the file doesn't exist or is invalid.
|
|
21
|
+
*
|
|
22
|
+
* @returns The parsed configuration.
|
|
23
|
+
*/
|
|
24
|
+
export function loadConfig(): OctoConfig {
|
|
25
|
+
if (!existsSync(CONFIG_FILE)) return {};
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8')) as OctoConfig;
|
|
28
|
+
} catch {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Saves the octo configuration to ~/.octo-config.json.
|
|
35
|
+
* Merges with existing config (does not overwrite unrelated keys).
|
|
36
|
+
*
|
|
37
|
+
* @param partial - Partial config to merge and persist.
|
|
38
|
+
*/
|
|
39
|
+
export function saveConfig(partial: Partial<OctoConfig>): void {
|
|
40
|
+
const existing = loadConfig();
|
|
41
|
+
const merged = { ...existing, ...partial };
|
|
42
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), 'utf-8');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Returns the path to the config file for display purposes.
|
|
47
|
+
*/
|
|
48
|
+
export function getConfigPath(): string {
|
|
49
|
+
return CONFIG_FILE;
|
|
50
|
+
}
|
package/src/shared/git-auth.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { askSecret } from './prompt.js';
|
|
2
2
|
|
|
3
3
|
let cachedToken: string | undefined;
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Acquires a GitHub Personal Access Token for git HTTPS operations.
|
|
7
|
-
* Prompts the user once and caches in memory for the process lifetime.
|
|
8
|
-
* The token is never persisted to disk.
|
|
7
|
+
* Prompts the user once (input hidden) and caches in memory for the process lifetime.
|
|
8
|
+
* The token is never persisted to disk or echoed to the terminal.
|
|
9
9
|
*
|
|
10
10
|
* @returns The PAT string, or undefined if the user provides empty input (skip).
|
|
11
11
|
*/
|
|
12
12
|
export async function acquireGitToken(): Promise<string | undefined> {
|
|
13
13
|
if (cachedToken) return cachedToken;
|
|
14
|
-
const input = await
|
|
14
|
+
const input = await askSecret('GitHub token (PAT): ');
|
|
15
15
|
cachedToken = input || undefined;
|
|
16
16
|
return cachedToken;
|
|
17
17
|
}
|
package/src/shared/llm.ts
CHANGED
|
@@ -1,69 +1,78 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { generateText } from 'ai';
|
|
2
|
+
import { createOpenAI } from '@ai-sdk/openai';
|
|
3
|
+
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
4
|
+
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
2
5
|
import { logger } from './logger.js';
|
|
3
|
-
|
|
4
|
-
const MODEL_ID = 'onnx-community/Qwen2.5-0.5B-Instruct';
|
|
5
|
-
|
|
6
|
-
let generator: TextGenerationPipeline | null = null;
|
|
7
|
-
let initFailed = false;
|
|
6
|
+
import { loadConfig, type LlmConfig } from './config.js';
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Resolves the AI SDK language model from the stored config.
|
|
10
|
+
* Supports openai, gemini, groq, anthropic, and any OpenAI-compatible custom endpoint.
|
|
11
|
+
*
|
|
12
|
+
* @param config - The LLM configuration with provider, baseUrl, model, and apiKey.
|
|
13
|
+
* @returns An AI SDK language model instance.
|
|
12
14
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
15
|
+
function resolveModel(config: LlmConfig) {
|
|
16
|
+
switch (config.provider) {
|
|
17
|
+
case 'google':
|
|
18
|
+
case 'gemini': {
|
|
19
|
+
const google = createGoogleGenerativeAI({ apiKey: config.apiKey });
|
|
20
|
+
return google(config.model);
|
|
21
|
+
}
|
|
22
|
+
case 'anthropic': {
|
|
23
|
+
const anthropic = createAnthropic({ apiKey: config.apiKey });
|
|
24
|
+
return anthropic(config.model);
|
|
25
|
+
}
|
|
26
|
+
case 'openai':
|
|
27
|
+
case 'groq':
|
|
28
|
+
case 'custom':
|
|
29
|
+
default: {
|
|
30
|
+
const openai = createOpenAI({ apiKey: config.apiKey, baseURL: config.baseUrl });
|
|
31
|
+
return openai(config.model);
|
|
32
|
+
}
|
|
28
33
|
}
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
/**
|
|
32
|
-
* Generates text from a prompt using the
|
|
33
|
-
* Returns null if
|
|
37
|
+
* Generates text from a prompt using the configured LLM provider.
|
|
38
|
+
* Returns null if no LLM is configured or the request fails.
|
|
39
|
+
*
|
|
40
|
+
* @param prompt - The user prompt to send.
|
|
41
|
+
* @param maxTokens - Maximum tokens in the response.
|
|
42
|
+
* @returns The generated text, or null on failure.
|
|
34
43
|
*/
|
|
35
44
|
export async function generate(prompt: string, maxTokens = 512): Promise<string | null> {
|
|
36
|
-
const
|
|
37
|
-
if (!
|
|
45
|
+
const config = loadConfig();
|
|
46
|
+
if (!config.llm?.apiKey) return null;
|
|
38
47
|
|
|
39
48
|
try {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
const model = resolveModel(config.llm);
|
|
50
|
+
const { text } = await generateText({
|
|
51
|
+
model,
|
|
52
|
+
prompt,
|
|
53
|
+
maxTokens,
|
|
54
|
+
temperature: 0,
|
|
46
55
|
});
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
return typeof output === 'string' ? output : null;
|
|
53
|
-
} catch {
|
|
56
|
+
return text?.trim() || null;
|
|
57
|
+
} catch (err) {
|
|
58
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
59
|
+
logger.warn(`LLM request failed: ${msg}. Using deterministic fallback.`);
|
|
54
60
|
return null;
|
|
55
61
|
}
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
/**
|
|
59
65
|
* Generates structured JSON output from a prompt.
|
|
60
|
-
* Returns null if parsing fails or
|
|
66
|
+
* Returns null if parsing fails or LLM is unavailable.
|
|
67
|
+
*
|
|
68
|
+
* @param prompt - The prompt requesting JSON output.
|
|
69
|
+
* @param maxTokens - Maximum tokens in the response.
|
|
70
|
+
* @returns Parsed JSON object, or null on failure.
|
|
61
71
|
*/
|
|
62
72
|
export async function generateJSON<T = unknown>(prompt: string, maxTokens = 1024): Promise<T | null> {
|
|
63
73
|
const text = await generate(prompt, maxTokens);
|
|
64
74
|
if (!text) return null;
|
|
65
75
|
|
|
66
|
-
// Extract JSON from the response (model may wrap in markdown code blocks)
|
|
67
76
|
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
68
77
|
if (!jsonMatch) return null;
|
|
69
78
|
|
|
@@ -75,9 +84,11 @@ export async function generateJSON<T = unknown>(prompt: string, maxTokens = 1024
|
|
|
75
84
|
}
|
|
76
85
|
|
|
77
86
|
/**
|
|
78
|
-
* Checks if
|
|
87
|
+
* Checks if an LLM provider is configured.
|
|
88
|
+
*
|
|
89
|
+
* @returns true if LLM config exists with a valid apiKey.
|
|
79
90
|
*/
|
|
80
|
-
export
|
|
81
|
-
const
|
|
82
|
-
return
|
|
91
|
+
export function isAvailable(): boolean {
|
|
92
|
+
const config = loadConfig();
|
|
93
|
+
return !!config.llm?.apiKey;
|
|
83
94
|
}
|
package/src/shared/prompt.ts
CHANGED
|
@@ -16,6 +16,47 @@ export function ask(message: string): Promise<string> {
|
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Prompts the user for sensitive input (e.g. tokens, passwords).
|
|
21
|
+
* Input is not echoed to the terminal.
|
|
22
|
+
*
|
|
23
|
+
* @param message - The prompt message displayed to the user.
|
|
24
|
+
* @returns The trimmed secret string.
|
|
25
|
+
*/
|
|
26
|
+
export function askSecret(message: string): Promise<string> {
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
process.stdout.write(message);
|
|
29
|
+
|
|
30
|
+
const stdin = process.stdin;
|
|
31
|
+
const wasRaw = stdin.isRaw;
|
|
32
|
+
if (stdin.isTTY) stdin.setRawMode(true);
|
|
33
|
+
stdin.resume();
|
|
34
|
+
stdin.setEncoding('utf-8');
|
|
35
|
+
|
|
36
|
+
let input = '';
|
|
37
|
+
|
|
38
|
+
const onData = (char: string) => {
|
|
39
|
+
if (char === '\n' || char === '\r') {
|
|
40
|
+
stdin.removeListener('data', onData);
|
|
41
|
+
if (stdin.isTTY) stdin.setRawMode(wasRaw ?? false);
|
|
42
|
+
stdin.pause();
|
|
43
|
+
process.stdout.write('\n');
|
|
44
|
+
resolve(input.trim());
|
|
45
|
+
} else if (char === '\u0003') {
|
|
46
|
+
// Ctrl+C
|
|
47
|
+
process.exit(130);
|
|
48
|
+
} else if (char === '\u007F' || char === '\b') {
|
|
49
|
+
// Backspace
|
|
50
|
+
input = input.slice(0, -1);
|
|
51
|
+
} else {
|
|
52
|
+
input += char;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
stdin.on('data', onData);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
19
60
|
/**
|
|
20
61
|
* Prompts the user with a yes/no question via stdin.
|
|
21
62
|
* Accepts 'y', 'Y', 's', 'S' as affirmative responses.
|
package/src/shared/sync.ts
CHANGED
|
@@ -25,7 +25,7 @@ export async function ensureRepositories(manifest: OctoManifest, rootDir: string
|
|
|
25
25
|
|
|
26
26
|
if (pending.length === 0) return 0;
|
|
27
27
|
|
|
28
|
-
logger.info(`${pending.length} repository(ies) to clone
|
|
28
|
+
logger.info(`${pending.length} repository(ies) to clone.\n`);
|
|
29
29
|
|
|
30
30
|
// Acquire token once for all clones
|
|
31
31
|
const token = await acquireGitToken();
|