@colinmollenhour/occtl 1.0.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/README.md +290 -0
- package/SKILL.md +692 -0
- package/dist/client.d.ts +4 -0
- package/dist/client.js +64 -0
- package/dist/commands/session-abort.d.ts +2 -0
- package/dist/commands/session-abort.js +16 -0
- package/dist/commands/session-children.d.ts +2 -0
- package/dist/commands/session-children.js +30 -0
- package/dist/commands/session-create.d.ts +2 -0
- package/dist/commands/session-create.js +39 -0
- package/dist/commands/session-diff.d.ts +2 -0
- package/dist/commands/session-diff.js +35 -0
- package/dist/commands/session-get.d.ts +2 -0
- package/dist/commands/session-get.js +24 -0
- package/dist/commands/session-last.d.ts +2 -0
- package/dist/commands/session-last.js +39 -0
- package/dist/commands/session-list.d.ts +2 -0
- package/dist/commands/session-list.js +91 -0
- package/dist/commands/session-messages.d.ts +2 -0
- package/dist/commands/session-messages.js +44 -0
- package/dist/commands/session-respond.d.ts +2 -0
- package/dist/commands/session-respond.js +78 -0
- package/dist/commands/session-send.d.ts +2 -0
- package/dist/commands/session-send.js +114 -0
- package/dist/commands/session-share.d.ts +3 -0
- package/dist/commands/session-share.js +53 -0
- package/dist/commands/session-status.d.ts +2 -0
- package/dist/commands/session-status.js +45 -0
- package/dist/commands/session-summary.d.ts +2 -0
- package/dist/commands/session-summary.js +87 -0
- package/dist/commands/session-todo.d.ts +2 -0
- package/dist/commands/session-todo.js +41 -0
- package/dist/commands/session-wait-for-text.d.ts +2 -0
- package/dist/commands/session-wait-for-text.js +119 -0
- package/dist/commands/session-wait.d.ts +4 -0
- package/dist/commands/session-wait.js +85 -0
- package/dist/commands/session-watch.d.ts +2 -0
- package/dist/commands/session-watch.js +101 -0
- package/dist/commands/skill.d.ts +3 -0
- package/dist/commands/skill.js +55 -0
- package/dist/commands/worktree.d.ts +5 -0
- package/dist/commands/worktree.js +359 -0
- package/dist/format.d.ts +19 -0
- package/dist/format.js +115 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +63 -0
- package/dist/resolve.d.ts +6 -0
- package/dist/resolve.js +47 -0
- package/dist/sse.d.ts +40 -0
- package/dist/sse.js +128 -0
- package/dist/wait-util.d.ts +23 -0
- package/dist/wait-util.js +118 -0
- package/package.json +49 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createOpencodeClient } from "@opencode-ai/sdk";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
let _client = null;
|
|
4
|
+
let _baseUrl = null;
|
|
5
|
+
/**
|
|
6
|
+
* Auto-detect the OpenCode server by looking at running processes.
|
|
7
|
+
* Falls back to env vars or defaults.
|
|
8
|
+
*/
|
|
9
|
+
function detectServer() {
|
|
10
|
+
// Check env vars first
|
|
11
|
+
if (process.env.OPENCODE_SERVER_HOST || process.env.OPENCODE_SERVER_PORT) {
|
|
12
|
+
return {
|
|
13
|
+
host: process.env.OPENCODE_SERVER_HOST || "127.0.0.1",
|
|
14
|
+
port: process.env.OPENCODE_SERVER_PORT || "4096",
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
// Try to detect from running opencode process
|
|
18
|
+
try {
|
|
19
|
+
const output = execSync("ps aux | grep 'opencode serve' | grep -v grep", { encoding: "utf-8", timeout: 2000 });
|
|
20
|
+
const lines = output.trim().split("\n");
|
|
21
|
+
for (const line of lines) {
|
|
22
|
+
const portMatch = line.match(/--port\s+(\d+)/);
|
|
23
|
+
const hostMatch = line.match(/--hostname\s+([\w.:]+)/);
|
|
24
|
+
if (portMatch) {
|
|
25
|
+
return {
|
|
26
|
+
host: hostMatch?.[1] || "127.0.0.1",
|
|
27
|
+
port: portMatch[1],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Process detection failed, fall through
|
|
34
|
+
}
|
|
35
|
+
return { host: "127.0.0.1", port: "4096" };
|
|
36
|
+
}
|
|
37
|
+
export function getBaseUrl() {
|
|
38
|
+
if (!_baseUrl) {
|
|
39
|
+
const { host, port } = detectServer();
|
|
40
|
+
_baseUrl = `http://${host}:${port}`;
|
|
41
|
+
}
|
|
42
|
+
return _baseUrl;
|
|
43
|
+
}
|
|
44
|
+
export function getClient() {
|
|
45
|
+
if (!_client) {
|
|
46
|
+
_client = createOpencodeClient({
|
|
47
|
+
baseUrl: getBaseUrl(),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return _client;
|
|
51
|
+
}
|
|
52
|
+
export async function ensureServer() {
|
|
53
|
+
const client = getClient();
|
|
54
|
+
try {
|
|
55
|
+
// Try listing sessions as a health check
|
|
56
|
+
await client.session.list();
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
console.error("Error: Cannot connect to OpenCode server at " + getBaseUrl());
|
|
60
|
+
console.error("Make sure OpenCode is running, or set OPENCODE_SERVER_HOST/OPENCODE_SERVER_PORT");
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
return client;
|
|
64
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { ensureServer } from "../client.js";
|
|
3
|
+
import { resolveSession } from "../resolve.js";
|
|
4
|
+
export function sessionAbortCommand() {
|
|
5
|
+
return new Command("abort")
|
|
6
|
+
.description("Abort a running session")
|
|
7
|
+
.argument("[session-id]", "Session ID (defaults to most recent)")
|
|
8
|
+
.action(async (sessionId) => {
|
|
9
|
+
const client = await ensureServer();
|
|
10
|
+
const resolved = await resolveSession(client, sessionId);
|
|
11
|
+
await client.session.abort({
|
|
12
|
+
path: { id: resolved },
|
|
13
|
+
});
|
|
14
|
+
console.log(`Aborted session: ${resolved}`);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { ensureServer } from "../client.js";
|
|
3
|
+
import { resolveSession } from "../resolve.js";
|
|
4
|
+
import { formatSession, formatJSON } from "../format.js";
|
|
5
|
+
export function sessionChildrenCommand() {
|
|
6
|
+
return new Command("children")
|
|
7
|
+
.description("List child sessions (sub-agents) of a session")
|
|
8
|
+
.argument("[session-id]", "Session ID (defaults to most recent)")
|
|
9
|
+
.option("-j, --json", "Output as JSON")
|
|
10
|
+
.action(async (sessionId, opts) => {
|
|
11
|
+
const client = await ensureServer();
|
|
12
|
+
const resolved = await resolveSession(client, sessionId);
|
|
13
|
+
const result = await client.session.children({
|
|
14
|
+
path: { id: resolved },
|
|
15
|
+
});
|
|
16
|
+
const children = result.data ?? [];
|
|
17
|
+
if (opts.json) {
|
|
18
|
+
console.log(formatJSON(children));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (children.length === 0) {
|
|
22
|
+
console.log("No child sessions.");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
console.log("ID\tTITLE\tUPDATED");
|
|
26
|
+
for (const s of children) {
|
|
27
|
+
console.log(formatSession(s));
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { ensureServer } from "../client.js";
|
|
3
|
+
import { formatSessionDetailed, formatJSON } from "../format.js";
|
|
4
|
+
export function sessionCreateCommand() {
|
|
5
|
+
return new Command("create")
|
|
6
|
+
.alias("new")
|
|
7
|
+
.description("Create a new session")
|
|
8
|
+
.option("-t, --title <title>", "Session title")
|
|
9
|
+
.option("-p, --parent <id>", "Parent session ID (creates a child/sub-agent session)")
|
|
10
|
+
.option("-d, --dir <path>", "Project directory for the session (defaults to cwd)")
|
|
11
|
+
.option("-j, --json", "Output as JSON")
|
|
12
|
+
.option("-q, --quiet", "Only output the session ID")
|
|
13
|
+
.action(async (opts) => {
|
|
14
|
+
const client = await ensureServer();
|
|
15
|
+
const directory = opts.dir
|
|
16
|
+
? (await import("path")).resolve(opts.dir)
|
|
17
|
+
: process.cwd();
|
|
18
|
+
const result = await client.session.create({
|
|
19
|
+
body: {
|
|
20
|
+
...(opts.title && { title: opts.title }),
|
|
21
|
+
...(opts.parent && { parentID: opts.parent }),
|
|
22
|
+
},
|
|
23
|
+
query: { directory },
|
|
24
|
+
});
|
|
25
|
+
if (!result.data) {
|
|
26
|
+
console.error("Failed to create session.");
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
if (opts.quiet) {
|
|
30
|
+
console.log(result.data.id);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (opts.json) {
|
|
34
|
+
console.log(formatJSON(result.data));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
console.log(formatSessionDetailed(result.data));
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { ensureServer } from "../client.js";
|
|
3
|
+
import { resolveSession } from "../resolve.js";
|
|
4
|
+
import { formatJSON } from "../format.js";
|
|
5
|
+
export function sessionDiffCommand() {
|
|
6
|
+
return new Command("diff")
|
|
7
|
+
.description("Show the diff (file changes) for a session")
|
|
8
|
+
.argument("[session-id]", "Session ID (defaults to most recent)")
|
|
9
|
+
.option("-j, --json", "Output as JSON")
|
|
10
|
+
.action(async (sessionId, opts) => {
|
|
11
|
+
const client = await ensureServer();
|
|
12
|
+
const resolved = await resolveSession(client, sessionId);
|
|
13
|
+
const result = await client.session.diff({
|
|
14
|
+
path: { id: resolved },
|
|
15
|
+
});
|
|
16
|
+
const diffs = result.data ?? [];
|
|
17
|
+
if (opts.json) {
|
|
18
|
+
console.log(formatJSON(diffs));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (diffs.length === 0) {
|
|
22
|
+
console.log("No file changes in this session.");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
for (const d of diffs) {
|
|
26
|
+
console.log(`--- ${d.file}`);
|
|
27
|
+
console.log(`+${d.additions} -${d.deletions}`);
|
|
28
|
+
if (d.before !== d.after) {
|
|
29
|
+
console.log(`Before: ${d.before.slice(0, 100)}`);
|
|
30
|
+
console.log(`After: ${d.after.slice(0, 100)}`);
|
|
31
|
+
}
|
|
32
|
+
console.log("");
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { ensureServer } from "../client.js";
|
|
3
|
+
import { formatSessionDetailed, formatJSON } from "../format.js";
|
|
4
|
+
export function sessionGetCommand() {
|
|
5
|
+
return new Command("get")
|
|
6
|
+
.description("Get session details")
|
|
7
|
+
.argument("<session-id>", "Session ID")
|
|
8
|
+
.option("-j, --json", "Output as JSON")
|
|
9
|
+
.action(async (sessionId, opts) => {
|
|
10
|
+
const client = await ensureServer();
|
|
11
|
+
const result = await client.session.get({
|
|
12
|
+
path: { id: sessionId },
|
|
13
|
+
});
|
|
14
|
+
if (!result.data) {
|
|
15
|
+
console.error(`Session not found: ${sessionId}`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
if (opts.json) {
|
|
19
|
+
console.log(formatJSON(result.data));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
console.log(formatSessionDetailed(result.data));
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { ensureServer } from "../client.js";
|
|
3
|
+
import { formatMessage, formatJSON } from "../format.js";
|
|
4
|
+
import { resolveSession } from "../resolve.js";
|
|
5
|
+
export function sessionLastCommand() {
|
|
6
|
+
return new Command("last")
|
|
7
|
+
.description("Get the last message from a session")
|
|
8
|
+
.argument("[session-id]", "Session ID (defaults to most recent)")
|
|
9
|
+
.option("-j, --json", "Output as JSON")
|
|
10
|
+
.option("-v, --verbose", "Show tool calls and extra details")
|
|
11
|
+
.option("-t, --text-only", "Show only text content (default)")
|
|
12
|
+
.option("--role <role>", "Get last message of a specific role (user or assistant)")
|
|
13
|
+
.action(async (sessionId, opts) => {
|
|
14
|
+
const client = await ensureServer();
|
|
15
|
+
const resolved = await resolveSession(client, sessionId);
|
|
16
|
+
const result = await client.session.messages({
|
|
17
|
+
path: { id: resolved },
|
|
18
|
+
});
|
|
19
|
+
let messages = result.data ?? [];
|
|
20
|
+
if (opts.role) {
|
|
21
|
+
messages = messages.filter((m) => m.info.role === opts.role);
|
|
22
|
+
}
|
|
23
|
+
if (messages.length === 0) {
|
|
24
|
+
console.error("No messages in session.");
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
const last = messages[messages.length - 1];
|
|
28
|
+
if (opts.json) {
|
|
29
|
+
console.log(formatJSON(last));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// Default to text-only for the 'last' command unless verbose
|
|
33
|
+
const textOnly = opts.verbose ? false : (opts.textOnly !== false);
|
|
34
|
+
console.log(formatMessage(last.info, last.parts, {
|
|
35
|
+
verbose: opts.verbose,
|
|
36
|
+
textOnly,
|
|
37
|
+
}));
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { ensureServer } from "../client.js";
|
|
4
|
+
import { formatSession, formatSessionDetailed, formatJSON } from "../format.js";
|
|
5
|
+
export function sessionListCommand() {
|
|
6
|
+
return new Command("list")
|
|
7
|
+
.alias("ls")
|
|
8
|
+
.description("List sessions. Default: current directory only. Pass a path to filter by directory, or --all for everything.")
|
|
9
|
+
.argument("[directory]", "Only show sessions for this directory")
|
|
10
|
+
.option("-j, --json", "Output as JSON")
|
|
11
|
+
.option("-d, --detailed", "Show detailed info for each session")
|
|
12
|
+
.option("-n, --limit <n>", "Limit number of results", parseInt)
|
|
13
|
+
.option("-a, --all", "Show sessions for all directories")
|
|
14
|
+
.option("-c, --children", "Include child sessions (sub-agents)")
|
|
15
|
+
.option("--sort <field>", "Sort by: updated (default), created, title", "updated")
|
|
16
|
+
.option("--asc", "Sort ascending instead of descending")
|
|
17
|
+
.action(async (directory, opts) => {
|
|
18
|
+
const client = await ensureServer();
|
|
19
|
+
// Determine which directory to filter by
|
|
20
|
+
let filterDir;
|
|
21
|
+
if (opts.all) {
|
|
22
|
+
filterDir = undefined;
|
|
23
|
+
}
|
|
24
|
+
else if (directory) {
|
|
25
|
+
filterDir = path.resolve(directory);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
filterDir = process.cwd();
|
|
29
|
+
}
|
|
30
|
+
const result = await client.session.list({
|
|
31
|
+
...(filterDir && { query: { directory: filterDir } }),
|
|
32
|
+
});
|
|
33
|
+
let sessions = result.data ?? [];
|
|
34
|
+
// Client-side directory filtering as fallback
|
|
35
|
+
if (filterDir) {
|
|
36
|
+
sessions = sessions.filter((s) => s.directory === filterDir);
|
|
37
|
+
}
|
|
38
|
+
// Filter out child sessions unless --children
|
|
39
|
+
if (!opts.children) {
|
|
40
|
+
sessions = sessions.filter((s) => !s.parentID);
|
|
41
|
+
}
|
|
42
|
+
// Sort
|
|
43
|
+
const sortField = opts.sort;
|
|
44
|
+
const ascending = !!opts.asc;
|
|
45
|
+
sessions.sort((a, b) => {
|
|
46
|
+
let cmp = 0;
|
|
47
|
+
switch (sortField) {
|
|
48
|
+
case "created":
|
|
49
|
+
cmp = b.time.created - a.time.created;
|
|
50
|
+
break;
|
|
51
|
+
case "title":
|
|
52
|
+
cmp = (a.title || "").localeCompare(b.title || "");
|
|
53
|
+
break;
|
|
54
|
+
case "updated":
|
|
55
|
+
default:
|
|
56
|
+
cmp = b.time.updated - a.time.updated;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
return ascending ? -cmp : cmp;
|
|
60
|
+
});
|
|
61
|
+
// Apply limit
|
|
62
|
+
if (opts.limit && opts.limit > 0) {
|
|
63
|
+
sessions = sessions.slice(0, opts.limit);
|
|
64
|
+
}
|
|
65
|
+
if (opts.json) {
|
|
66
|
+
console.log(formatJSON(sessions));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (sessions.length === 0) {
|
|
70
|
+
if (filterDir) {
|
|
71
|
+
console.log(`No sessions found for ${filterDir}.`);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
console.log("No sessions found.");
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (opts.detailed) {
|
|
79
|
+
for (const s of sessions) {
|
|
80
|
+
console.log(formatSessionDetailed(s));
|
|
81
|
+
console.log("");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
console.log("ID\tTITLE\tUPDATED");
|
|
86
|
+
for (const s of sessions) {
|
|
87
|
+
console.log(formatSession(s));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { ensureServer } from "../client.js";
|
|
3
|
+
import { formatMessage, formatJSON } from "../format.js";
|
|
4
|
+
import { resolveSession } from "../resolve.js";
|
|
5
|
+
export function sessionMessagesCommand() {
|
|
6
|
+
return new Command("messages")
|
|
7
|
+
.alias("msgs")
|
|
8
|
+
.description("List messages in a session")
|
|
9
|
+
.argument("[session-id]", "Session ID (defaults to most recent)")
|
|
10
|
+
.option("-j, --json", "Output as JSON")
|
|
11
|
+
.option("-v, --verbose", "Show tool calls and extra details")
|
|
12
|
+
.option("-t, --text-only", "Show only text content")
|
|
13
|
+
.option("-n, --limit <n>", "Limit number of messages", parseInt)
|
|
14
|
+
.option("--role <role>", "Filter by role (user or assistant)")
|
|
15
|
+
.action(async (sessionId, opts) => {
|
|
16
|
+
const client = await ensureServer();
|
|
17
|
+
const resolved = await resolveSession(client, sessionId);
|
|
18
|
+
const result = await client.session.messages({
|
|
19
|
+
path: { id: resolved },
|
|
20
|
+
});
|
|
21
|
+
let messages = result.data ?? [];
|
|
22
|
+
if (opts.role) {
|
|
23
|
+
messages = messages.filter((m) => m.info.role === opts.role);
|
|
24
|
+
}
|
|
25
|
+
if (opts.limit && opts.limit > 0) {
|
|
26
|
+
messages = messages.slice(-opts.limit);
|
|
27
|
+
}
|
|
28
|
+
if (opts.json) {
|
|
29
|
+
console.log(formatJSON(messages));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (messages.length === 0) {
|
|
33
|
+
console.log("No messages in session.");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
for (const m of messages) {
|
|
37
|
+
console.log(formatMessage(m.info, m.parts, {
|
|
38
|
+
verbose: opts.verbose,
|
|
39
|
+
textOnly: opts.textOnly,
|
|
40
|
+
}));
|
|
41
|
+
console.log("");
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { ensureServer, getClient } from "../client.js";
|
|
3
|
+
import { resolveSession } from "../resolve.js";
|
|
4
|
+
import { formatJSON } from "../format.js";
|
|
5
|
+
import { streamEvents } from "../sse.js";
|
|
6
|
+
export function sessionRespondCommand() {
|
|
7
|
+
return new Command("respond")
|
|
8
|
+
.description("Respond to a permission request in a session")
|
|
9
|
+
.argument("[session-id]", "Session ID (defaults to most recent)")
|
|
10
|
+
.option("-r, --response <response>", "Response: once, always, or reject", "once")
|
|
11
|
+
.option("-j, --json", "Output as JSON")
|
|
12
|
+
.option("-p, --permission-id <id>", "Permission ID to respond to (auto-detects if omitted)")
|
|
13
|
+
.option("-w, --wait", "Wait for a permission request if none pending")
|
|
14
|
+
.option("--auto-approve", "Automatically approve all permission requests (implies --wait)")
|
|
15
|
+
.action(async (sessionId, opts) => {
|
|
16
|
+
const client = await ensureServer();
|
|
17
|
+
const resolved = await resolveSession(client, sessionId);
|
|
18
|
+
const validResponses = ["once", "always", "reject"];
|
|
19
|
+
if (!validResponses.includes(opts.response)) {
|
|
20
|
+
console.error(`Invalid response: ${opts.response}. Must be one of: ${validResponses.join(", ")}`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
if (opts.permissionId) {
|
|
24
|
+
await respondToPermission(resolved, opts.permissionId, opts.response);
|
|
25
|
+
if (opts.json) {
|
|
26
|
+
console.log(formatJSON({ success: true, permissionId: opts.permissionId }));
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
console.log(`Responded to permission ${opts.permissionId} with: ${opts.response}`);
|
|
30
|
+
}
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (opts.wait || opts.autoApprove) {
|
|
34
|
+
await waitAndRespond(resolved, opts);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
console.error("No --permission-id specified. Use --wait to wait for permission requests.");
|
|
38
|
+
process.exit(1);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
async function respondToPermission(sessionId, permissionId, response) {
|
|
42
|
+
const client = getClient();
|
|
43
|
+
await client.postSessionIdPermissionsPermissionId({
|
|
44
|
+
path: { id: sessionId, permissionID: permissionId },
|
|
45
|
+
body: { response },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async function waitAndRespond(sessionId, opts) {
|
|
49
|
+
console.error(`Waiting for permission requests on session ${sessionId}...`);
|
|
50
|
+
console.error("Press Ctrl+C to stop.\n");
|
|
51
|
+
const result = await streamEvents(sessionId, async (event) => {
|
|
52
|
+
if (event.type !== "permission.updated")
|
|
53
|
+
return;
|
|
54
|
+
const props = event.properties;
|
|
55
|
+
// Skip permissions that are not pending (already resolved)
|
|
56
|
+
if (props.status && props.status !== "pending")
|
|
57
|
+
return;
|
|
58
|
+
console.error(`Permission request: ${props.title} (type: ${props.type}, id: ${props.id})`);
|
|
59
|
+
try {
|
|
60
|
+
if (opts.autoApprove) {
|
|
61
|
+
await respondToPermission(sessionId, props.id, "once");
|
|
62
|
+
console.error(`Auto-approved: ${props.id}`);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
await respondToPermission(sessionId, props.id, opts.response);
|
|
66
|
+
console.error(`Responded with "${opts.response}": ${props.id}`);
|
|
67
|
+
return "stop";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
console.error(`Failed to respond to ${props.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
if (result === "disconnected") {
|
|
75
|
+
console.error("Error: lost connection to OpenCode server.");
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { ensureServer } from "../client.js";
|
|
3
|
+
import { formatMessage, formatJSON } from "../format.js";
|
|
4
|
+
import { resolveSession } from "../resolve.js";
|
|
5
|
+
import { waitForIdle } from "../wait-util.js";
|
|
6
|
+
export function sessionSendCommand() {
|
|
7
|
+
return new Command("send")
|
|
8
|
+
.alias("prompt")
|
|
9
|
+
.description("Send a message to a session")
|
|
10
|
+
.argument("<message...>", "Message text to send")
|
|
11
|
+
.option("-s, --session <id>", "Session ID (defaults to most recent)")
|
|
12
|
+
.option("-j, --json", "Output response as JSON")
|
|
13
|
+
.option("-v, --verbose", "Show tool calls and extra details")
|
|
14
|
+
.option("-t, --text-only", "Show only text content in response")
|
|
15
|
+
.option("--no-reply", "Send as context injection (no AI response)")
|
|
16
|
+
.option("--async", "Send async and return immediately")
|
|
17
|
+
.option("-w, --wait", "Send async, block until session is idle, then show the last message")
|
|
18
|
+
.option("--agent <agent>", "Agent to use")
|
|
19
|
+
.option("--model <model>", "Model to use (format: provider/model)")
|
|
20
|
+
.option("--stdin", "Read message from stdin instead of arguments")
|
|
21
|
+
.action(async (messageParts, opts) => {
|
|
22
|
+
const client = await ensureServer();
|
|
23
|
+
const resolved = await resolveSession(client, opts.session);
|
|
24
|
+
let messageText;
|
|
25
|
+
if (opts.stdin) {
|
|
26
|
+
const chunks = [];
|
|
27
|
+
for await (const chunk of process.stdin) {
|
|
28
|
+
chunks.push(chunk);
|
|
29
|
+
}
|
|
30
|
+
messageText = Buffer.concat(chunks).toString("utf-8").trim();
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
messageText = messageParts.join(" ");
|
|
34
|
+
}
|
|
35
|
+
if (!messageText) {
|
|
36
|
+
console.error("No message provided.");
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
// Parse model if provided
|
|
40
|
+
let model;
|
|
41
|
+
if (opts.model) {
|
|
42
|
+
const parts = opts.model.split("/");
|
|
43
|
+
if (parts.length === 2 && parts[0] && parts[1]) {
|
|
44
|
+
model = { providerID: parts[0], modelID: parts[1] };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const body = {
|
|
48
|
+
parts: [{ type: "text", text: messageText }],
|
|
49
|
+
...(model && { model }),
|
|
50
|
+
...(opts.agent && { agent: opts.agent }),
|
|
51
|
+
...(opts.reply === false && { noReply: true }),
|
|
52
|
+
};
|
|
53
|
+
// --async: fire and forget
|
|
54
|
+
if (opts.async) {
|
|
55
|
+
await client.session.promptAsync({
|
|
56
|
+
path: { id: resolved },
|
|
57
|
+
body,
|
|
58
|
+
});
|
|
59
|
+
console.log("Message sent (async).");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// --wait: send async, then block until session.idle, then show result
|
|
63
|
+
if (opts.wait) {
|
|
64
|
+
await client.session.promptAsync({
|
|
65
|
+
path: { id: resolved },
|
|
66
|
+
body,
|
|
67
|
+
});
|
|
68
|
+
const result = await waitForIdle(client, resolved);
|
|
69
|
+
if (!result.idle) {
|
|
70
|
+
if (result.reason === "disconnected") {
|
|
71
|
+
console.error("Error: lost connection to OpenCode server.");
|
|
72
|
+
}
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
// Fetch and display the last assistant message
|
|
76
|
+
const msgs = await client.session.messages({
|
|
77
|
+
path: { id: resolved },
|
|
78
|
+
});
|
|
79
|
+
const messages = msgs.data ?? [];
|
|
80
|
+
const last = messages.filter((m) => m.info.role === "assistant").pop();
|
|
81
|
+
if (!last) {
|
|
82
|
+
console.error("No assistant response found.");
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
if (opts.json) {
|
|
86
|
+
console.log(formatJSON(last));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const textOnly = opts.verbose ? false : (opts.textOnly !== false);
|
|
90
|
+
console.log(formatMessage(last.info, last.parts, {
|
|
91
|
+
verbose: opts.verbose,
|
|
92
|
+
textOnly,
|
|
93
|
+
}));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
// Default: synchronous send (blocks until response)
|
|
97
|
+
const syncResult = await client.session.prompt({
|
|
98
|
+
path: { id: resolved },
|
|
99
|
+
body,
|
|
100
|
+
});
|
|
101
|
+
if (!syncResult.data) {
|
|
102
|
+
console.error("No response received.");
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
if (opts.json) {
|
|
106
|
+
console.log(formatJSON(syncResult.data));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
console.log(formatMessage(syncResult.data.info, syncResult.data.parts, {
|
|
110
|
+
verbose: opts.verbose,
|
|
111
|
+
textOnly: opts.textOnly ?? true,
|
|
112
|
+
}));
|
|
113
|
+
});
|
|
114
|
+
}
|