@inceptionstack/roundhouse 0.4.2 → 0.4.4
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 +27 -18
- package/architecture.md +10 -4
- package/package.json +1 -6
- package/pi/extensions/web-search.ts +1 -9
- package/src/agents/base-adapter.ts +92 -0
- package/src/agents/index.ts +7 -0
- package/src/agents/kiro/acp/client.ts +117 -0
- package/src/agents/kiro/acp/index.ts +8 -0
- package/src/agents/kiro/acp/process.ts +111 -0
- package/src/agents/kiro/acp/types.ts +83 -0
- package/src/agents/kiro/kiro-adapter.ts +448 -0
- package/src/agents/kiro/session.ts +124 -0
- package/src/agents/kiro/tool-names.ts +37 -0
- package/src/agents/{pi.ts → pi/pi-adapter.ts} +8 -5
- package/src/agents/registry.ts +26 -4
- package/src/bundle.ts +83 -2
- package/src/cli/cli.ts +38 -2
- package/src/cli/doctor/checks/credentials.ts +2 -2
- package/src/cli/env-file.ts +25 -0
- package/src/cli/setup.ts +16 -5
- package/src/gateway.ts +12 -0
- package/src/types.ts +66 -20
package/src/bundle.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
9
|
import { resolve, dirname } from "node:path";
|
|
10
|
-
import { readFileSync, mkdirSync, writeFileSync, existsSync, readdirSync } from "node:fs";
|
|
10
|
+
import { readFileSync, mkdirSync, writeFileSync, existsSync, readdirSync, copyFileSync } from "node:fs";
|
|
11
11
|
import { execFileSync } from "node:child_process";
|
|
12
12
|
import { randomBytes } from "node:crypto";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
@@ -183,7 +183,7 @@ export function provisionMcporterConfig(opts: ProvisionOpts = {}): void {
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
/**
|
|
186
|
-
* Provision all bundle dependencies (skills + CLI tools + config).
|
|
186
|
+
* Provision all bundle dependencies (skills + CLI tools + config + extensions).
|
|
187
187
|
* Non-fatal — logs warnings on failure but never throws.
|
|
188
188
|
*/
|
|
189
189
|
export function provisionBundle(opts: ProvisionOpts = {}): void {
|
|
@@ -192,4 +192,85 @@ export function provisionBundle(opts: ProvisionOpts = {}): void {
|
|
|
192
192
|
provisionPlaywright(opts);
|
|
193
193
|
provisionUvx(opts);
|
|
194
194
|
provisionMcporterConfig(opts);
|
|
195
|
+
provisionExtensionFiles(opts);
|
|
196
|
+
provisionExtensions(opts);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Copy shipped extension files to ~/.pi/agent/extensions/ if not already present.
|
|
201
|
+
* Does NOT overwrite existing files — user's version always wins.
|
|
202
|
+
*/
|
|
203
|
+
export function provisionExtensionFiles(opts: ProvisionOpts = {}): void {
|
|
204
|
+
const { log = consoleLog } = opts;
|
|
205
|
+
const shippedDir = resolve(dirname(fileURLToPath(import.meta.url)), "..", "pi", "extensions");
|
|
206
|
+
const targetDir = resolve(homedir(), ".pi", "agent", "extensions");
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
if (!existsSync(shippedDir)) return;
|
|
210
|
+
const files = readdirSync(shippedDir).filter(f => f.endsWith(".ts"));
|
|
211
|
+
if (files.length === 0) return;
|
|
212
|
+
|
|
213
|
+
mkdirSync(targetDir, { recursive: true });
|
|
214
|
+
|
|
215
|
+
let installed = 0;
|
|
216
|
+
let skipped = 0;
|
|
217
|
+
for (const file of files) {
|
|
218
|
+
const target = resolve(targetDir, file);
|
|
219
|
+
if (existsSync(target)) {
|
|
220
|
+
skipped++;
|
|
221
|
+
} else {
|
|
222
|
+
copyFileSync(resolve(shippedDir, file), target);
|
|
223
|
+
installed++;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (installed > 0) {
|
|
228
|
+
log.ok(`${installed} extension(s) installed to ~/.pi/agent/extensions/`);
|
|
229
|
+
}
|
|
230
|
+
if (skipped > 0 && installed === 0) {
|
|
231
|
+
log.ok(`extensions (${skipped} already present, not overwritten)`);
|
|
232
|
+
}
|
|
233
|
+
} catch (err: any) {
|
|
234
|
+
log.warn(`extension file provisioning failed: ${err.message}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Ensure core extensions are listed in ~/.pi/agent/settings.json packages array.
|
|
240
|
+
*/
|
|
241
|
+
export function provisionExtensions(opts: ProvisionOpts = {}): void {
|
|
242
|
+
const { log = consoleLog } = opts;
|
|
243
|
+
const settingsPath = resolve(homedir(), ".pi", "agent", "settings.json");
|
|
244
|
+
|
|
245
|
+
const coreExtensions = [
|
|
246
|
+
"npm:@inceptionstack/pi-hard-no",
|
|
247
|
+
"npm:@inceptionstack/pi-branch-enforcer",
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
let settings: Record<string, unknown> = {};
|
|
252
|
+
if (existsSync(settingsPath)) {
|
|
253
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
254
|
+
}
|
|
255
|
+
if (!Array.isArray(settings.packages)) settings.packages = [];
|
|
256
|
+
const pkgs = settings.packages as string[];
|
|
257
|
+
|
|
258
|
+
let added = 0;
|
|
259
|
+
for (const ext of coreExtensions) {
|
|
260
|
+
if (!pkgs.includes(ext)) {
|
|
261
|
+
pkgs.push(ext);
|
|
262
|
+
added++;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (added > 0) {
|
|
267
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
268
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
269
|
+
log.ok(`${added} extension(s) added to settings.json`);
|
|
270
|
+
} else {
|
|
271
|
+
log.ok("extensions (already configured)");
|
|
272
|
+
}
|
|
273
|
+
} catch (err: any) {
|
|
274
|
+
log.warn(`extensions provisioning failed: ${err.message}`);
|
|
275
|
+
}
|
|
195
276
|
}
|
package/src/cli/cli.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
} from "../config";
|
|
25
25
|
import { getAgentSdkPackage } from "../agents/registry";
|
|
26
26
|
import { threadIdToDir } from "../util";
|
|
27
|
-
import { parseEnvFile, serializeEnvFile, envQuote } from "./env-file";
|
|
27
|
+
import { parseEnvFile, serializeEnvFile, envQuote, unquoteEnvValue } from "./env-file";
|
|
28
28
|
import {
|
|
29
29
|
SERVICE_PATH,
|
|
30
30
|
systemctl,
|
|
@@ -74,12 +74,20 @@ async function cmdStart() {
|
|
|
74
74
|
|
|
75
75
|
// No systemd service — fall back to foreground
|
|
76
76
|
console.log("No systemd service found. Running in foreground (use Ctrl+C to stop)...");
|
|
77
|
-
|
|
77
|
+
if (process.platform !== "darwin") {
|
|
78
|
+
console.log(" Tip: run 'roundhouse install' to set up the systemd daemon.\n");
|
|
79
|
+
} else {
|
|
80
|
+
console.log("");
|
|
81
|
+
}
|
|
78
82
|
await cmdRun();
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
async function cmdRun() {
|
|
82
86
|
process.env.ROUNDHOUSE_CONFIG = CONFIG_PATH;
|
|
87
|
+
|
|
88
|
+
// Load .env file so secrets (TELEGRAM_BOT_TOKEN, etc.) are available
|
|
89
|
+
await loadEnvFile();
|
|
90
|
+
|
|
83
91
|
const indexPath = resolve(__dirname, "..", "index.ts");
|
|
84
92
|
const jsPath = resolve(__dirname, "..", "dist", "index.js");
|
|
85
93
|
|
|
@@ -94,7 +102,35 @@ async function cmdRun() {
|
|
|
94
102
|
}
|
|
95
103
|
}
|
|
96
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Load the roundhouse .env file into process.env.
|
|
107
|
+
* Does NOT override existing env vars (explicit env takes precedence).
|
|
108
|
+
*/
|
|
109
|
+
async function loadEnvFile(): Promise<void> {
|
|
110
|
+
const envPath = await resolveEnvFilePath();
|
|
111
|
+
if (!(await fileExists(envPath))) return;
|
|
112
|
+
try {
|
|
113
|
+
const entries = parseEnvFile(await readFile(envPath, "utf8"));
|
|
114
|
+
for (const [key, raw] of entries) {
|
|
115
|
+
if (!process.env[key]) {
|
|
116
|
+
process.env[key] = unquoteEnvValue(raw);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch (e: any) {
|
|
120
|
+
console.warn(`[roundhouse] warning: failed to load ${envPath}: ${e.message}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
97
124
|
async function cmdInstall() {
|
|
125
|
+
if (process.platform === "darwin") {
|
|
126
|
+
console.log("[roundhouse] macOS detected — systemd is not available.\n");
|
|
127
|
+
console.log(" On macOS, use 'roundhouse start' to run in foreground,");
|
|
128
|
+
console.log(" or set up a launchd plist manually.\n");
|
|
129
|
+
console.log(" Tip: run 'roundhouse setup --telegram' to configure first.");
|
|
130
|
+
process.exitCode = 1;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
98
134
|
console.log("[roundhouse] Installing as systemd daemon...\n");
|
|
99
135
|
|
|
100
136
|
await mkdir(CONFIG_DIR, { recursive: true });
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { readFile } from "node:fs/promises";
|
|
7
|
-
import { parseEnvFile } from "../../env-file";
|
|
7
|
+
import { parseEnvFile, unquoteEnvValue } from "../../env-file";
|
|
8
8
|
import type { DoctorCheck, DoctorContext } from "../types";
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -17,7 +17,7 @@ export async function resolveToken(ctx: DoctorContext): Promise<string | null> {
|
|
|
17
17
|
try {
|
|
18
18
|
const entries = parseEnvFile(await readFile(ctx.envFilePath, "utf8"));
|
|
19
19
|
const raw = entries.get("TELEGRAM_BOT_TOKEN");
|
|
20
|
-
if (raw) token = raw
|
|
20
|
+
if (raw) token = unquoteEnvValue(raw);
|
|
21
21
|
} catch {}
|
|
22
22
|
}
|
|
23
23
|
return token || null;
|
package/src/cli/env-file.ts
CHANGED
|
@@ -38,3 +38,28 @@ export function envQuote(value: string): string {
|
|
|
38
38
|
.replace(/\n/g, "\\n");
|
|
39
39
|
return `"${escaped}"`;
|
|
40
40
|
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Reverse of envQuote: strip surrounding quotes and unescape interior sequences.
|
|
44
|
+
*/
|
|
45
|
+
export function unquoteEnvValue(raw: string): string {
|
|
46
|
+
// Strip surrounding double quotes (only if matched pair)
|
|
47
|
+
let value = raw;
|
|
48
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
49
|
+
value = value.slice(1, -1);
|
|
50
|
+
} else if (value.startsWith("'") && value.endsWith("'")) {
|
|
51
|
+
value = value.slice(1, -1);
|
|
52
|
+
}
|
|
53
|
+
// Single-pass unescape to handle \\ correctly (avoids double-replacement)
|
|
54
|
+
value = value.replace(/\\([\\"$`n])/g, (_match, ch) => {
|
|
55
|
+
switch (ch) {
|
|
56
|
+
case "n": return "\n";
|
|
57
|
+
case "\\": return "\\";
|
|
58
|
+
case '"': return '"';
|
|
59
|
+
case "$": return "$";
|
|
60
|
+
case "`": return "`";
|
|
61
|
+
default: return ch;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
return value;
|
|
65
|
+
}
|
package/src/cli/setup.ts
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
ENV_FILE_PATH as ENV_PATH,
|
|
23
23
|
fileExists,
|
|
24
24
|
} from "../config";
|
|
25
|
-
import { envQuote, parseEnvFile } from "./env-file";
|
|
25
|
+
import { envQuote, parseEnvFile, unquoteEnvValue } from "./env-file";
|
|
26
26
|
import {
|
|
27
27
|
whichSync,
|
|
28
28
|
systemctl,
|
|
@@ -132,10 +132,21 @@ function resolveAgentForSetup(opts: SetupOptions): AgentDefinition {
|
|
|
132
132
|
// Ensure packages array exists
|
|
133
133
|
if (!Array.isArray(settings.packages)) settings.packages = [];
|
|
134
134
|
|
|
135
|
-
// Add roundhouse itself (ships extensions via pi.extensions in package.json)
|
|
136
|
-
const selfPkg = "npm:@inceptionstack/roundhouse";
|
|
137
135
|
const pkgs = settings.packages as string[];
|
|
138
|
-
|
|
136
|
+
|
|
137
|
+
// Remove stale self-reference (roundhouse no longer ships pi extensions)
|
|
138
|
+
const selfPkg = "npm:@inceptionstack/roundhouse";
|
|
139
|
+
const selfIdx = pkgs.indexOf(selfPkg);
|
|
140
|
+
if (selfIdx !== -1) pkgs.splice(selfIdx, 1);
|
|
141
|
+
|
|
142
|
+
// Add code review + branch protection extensions
|
|
143
|
+
const coreExtensions = [
|
|
144
|
+
"npm:@inceptionstack/pi-hard-no",
|
|
145
|
+
"npm:@inceptionstack/pi-branch-enforcer",
|
|
146
|
+
];
|
|
147
|
+
for (const ext of coreExtensions) {
|
|
148
|
+
if (!pkgs.includes(ext)) pkgs.push(ext);
|
|
149
|
+
}
|
|
139
150
|
|
|
140
151
|
// Add pi-psst if using psst
|
|
141
152
|
if (ctx.psst) {
|
|
@@ -1246,7 +1257,7 @@ export async function cmdPair(argv: string[]): Promise<void> {
|
|
|
1246
1257
|
try {
|
|
1247
1258
|
const entries = parseEnvFile(await readFile(ENV_PATH, "utf8"));
|
|
1248
1259
|
const raw = entries.get("TELEGRAM_BOT_TOKEN");
|
|
1249
|
-
if (raw) token = raw
|
|
1260
|
+
if (raw) token = unquoteEnvValue(raw);
|
|
1250
1261
|
} catch {}
|
|
1251
1262
|
}
|
|
1252
1263
|
|
package/src/gateway.ts
CHANGED
|
@@ -639,6 +639,18 @@ export class Gateway {
|
|
|
639
639
|
lines.push(`📝 Context: no usage data yet (${windowK}K window)`);
|
|
640
640
|
}
|
|
641
641
|
|
|
642
|
+
// Extensions
|
|
643
|
+
const extensions = Array.isArray(info.extensions) ? info.extensions as string[] : [];
|
|
644
|
+
if (extensions.length > 0) {
|
|
645
|
+
lines.push(``);
|
|
646
|
+
lines.push(`🧩 Extensions (${extensions.length}):`);
|
|
647
|
+
for (const ext of extensions) {
|
|
648
|
+
// Show short name: strip npm: prefix and path noise
|
|
649
|
+
const short = ext.replace(/^.*node_modules\//, "").replace(/\/index\.[tj]s$/, "");
|
|
650
|
+
lines.push(` • ${short}`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
642
654
|
await this.postWithFallback(thread, lines.join("\n"));
|
|
643
655
|
console.log(`[roundhouse] /status for thread=${thread.id} agentThread=${agentThreadId}`);
|
|
644
656
|
return;
|
package/src/types.ts
CHANGED
|
@@ -45,41 +45,87 @@ export type AgentStreamEvent =
|
|
|
45
45
|
| { type: "agent_end" }
|
|
46
46
|
| { type: "custom_message"; customType: string; content: string };
|
|
47
47
|
|
|
48
|
+
// ── AdapterInfo ──────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Information returned by getInfo(). All fields optional.
|
|
52
|
+
* Consumers (gateway /status, memory lifecycle) read these keys.
|
|
53
|
+
*/
|
|
54
|
+
export interface AdapterInfo {
|
|
55
|
+
/** Agent SDK/CLI version string */
|
|
56
|
+
version?: string;
|
|
57
|
+
/** Currently active model identifier */
|
|
58
|
+
model?: string;
|
|
59
|
+
/** Working directory the agent operates in */
|
|
60
|
+
cwd?: string;
|
|
61
|
+
/** Number of active sessions managed by this adapter */
|
|
62
|
+
activeSessions?: number;
|
|
63
|
+
|
|
64
|
+
// ── Context usage (drives memory pressure detection) ─
|
|
65
|
+
|
|
66
|
+
/** Current token count in context */
|
|
67
|
+
contextTokens?: number | null;
|
|
68
|
+
/** Maximum context window size in tokens */
|
|
69
|
+
contextWindow?: number | null;
|
|
70
|
+
/** Percentage of context used (0-100) */
|
|
71
|
+
contextPercent?: number | null;
|
|
72
|
+
|
|
73
|
+
// ── Memory system integration ──────────────────────
|
|
74
|
+
|
|
75
|
+
/** Whether agent has its own memory extension (determines roundhouse memory mode) */
|
|
76
|
+
hasMemoryExtension?: boolean;
|
|
77
|
+
/** Names of memory-related tools the agent exposes */
|
|
78
|
+
memoryTools?: string[];
|
|
79
|
+
|
|
80
|
+
// ── Extensions / capabilities ──────────────────────
|
|
81
|
+
|
|
82
|
+
/** List of loaded extension paths/names */
|
|
83
|
+
extensions?: string[];
|
|
84
|
+
|
|
85
|
+
/** Additional adapter-specific fields */
|
|
86
|
+
[key: string]: unknown;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* AgentAdapter interface — the contract between gateway and adapters.
|
|
91
|
+
*
|
|
92
|
+
* New adapters should extend BaseAdapter (from ./agents/base-adapter.ts)
|
|
93
|
+
* which provides default implementations for optional methods.
|
|
94
|
+
*/
|
|
48
95
|
export interface AgentAdapter {
|
|
49
96
|
/** Unique agent name, e.g. "pi", "kiro" */
|
|
50
|
-
name: string;
|
|
97
|
+
readonly name: string;
|
|
98
|
+
|
|
99
|
+
// ── Required ─────────────────────────────────────────
|
|
51
100
|
|
|
52
101
|
/** Send a user message and return the full assistant response */
|
|
53
102
|
prompt(threadId: string, message: AgentMessage): Promise<AgentResponse>;
|
|
54
103
|
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
104
|
+
/** Send a user message and stream back events in real time. */
|
|
105
|
+
promptStream(threadId: string, message: AgentMessage): AsyncIterable<AgentStreamEvent>;
|
|
106
|
+
|
|
107
|
+
/** Tear down all sessions and release resources. */
|
|
108
|
+
dispose(): Promise<void>;
|
|
109
|
+
|
|
110
|
+
// ── Optional (have defaults in BaseAdapter) ──────────
|
|
60
111
|
|
|
61
|
-
/**
|
|
62
|
-
|
|
63
|
-
* Falls back to prompt() if not implemented.
|
|
64
|
-
*/
|
|
65
|
-
promptStream?(threadId: string, message: AgentMessage): AsyncIterable<AgentStreamEvent>;
|
|
112
|
+
/** Prompt using a specific model (e.g. Haiku for memory flush). */
|
|
113
|
+
promptWithModel?(threadId: string, message: AgentMessage, modelId: string): Promise<AgentResponse>;
|
|
66
114
|
|
|
67
|
-
/** Dispose the session for a thread and start fresh on next prompt */
|
|
115
|
+
/** Dispose the session for a thread and start fresh on next prompt. */
|
|
68
116
|
restart?(threadId: string): Promise<void>;
|
|
69
117
|
|
|
70
|
-
/** Compact the session context for a thread */
|
|
118
|
+
/** Compact the session context for a thread. */
|
|
71
119
|
compact?(threadId: string): Promise<{ tokensBefore: number; tokensAfter: number | null } | null>;
|
|
72
|
-
|
|
120
|
+
|
|
121
|
+
/** Compact with a specific model. */
|
|
73
122
|
compactWithModel?(threadId: string, modelId: string): Promise<{ tokensBefore: number; tokensAfter: number | null } | null>;
|
|
74
123
|
|
|
75
|
-
/** Abort the current agent run for a thread */
|
|
124
|
+
/** Abort the current agent run for a thread. */
|
|
76
125
|
abort?(threadId: string): Promise<void>;
|
|
77
126
|
|
|
78
|
-
/** Return runtime info about the agent (model, version, etc.) */
|
|
79
|
-
getInfo?(threadId?: string):
|
|
80
|
-
|
|
81
|
-
/** Tear down all sessions */
|
|
82
|
-
dispose(): Promise<void>;
|
|
127
|
+
/** Return runtime info about the agent (model, version, etc.). */
|
|
128
|
+
getInfo?(threadId?: string): AdapterInfo;
|
|
83
129
|
}
|
|
84
130
|
|
|
85
131
|
export interface AgentResponse {
|