@jancellor/ask 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/LICENSE +21 -0
- package/README.md +93 -0
- package/assets/system-prompt.md +107 -0
- package/dist/agent/agent.js +132 -0
- package/dist/agent/agents-prompt.js +23 -0
- package/dist/agent/config.js +54 -0
- package/dist/agent/execute-tool.js +80 -0
- package/dist/agent/index.js +1 -0
- package/dist/agent/init-prompt.js +11 -0
- package/dist/agent/messages.js +1 -0
- package/dist/agent/openai-subscription-fetch.js +199 -0
- package/dist/agent/paths.js +6 -0
- package/dist/agent/serializer.js +25 -0
- package/dist/agent/session-store.js +80 -0
- package/dist/agent/session.js +87 -0
- package/dist/agent/skills-prompt.js +56 -0
- package/dist/agent/system-prompt.js +11 -0
- package/dist/agent/system-prompt.md +107 -0
- package/dist/agent/tools.js +17 -0
- package/dist/batch/index.js +101 -0
- package/dist/index.js +32 -0
- package/dist/shutdown-manager.js +35 -0
- package/dist/tui/app.js +12 -0
- package/dist/tui/assistant-part-message.js +10 -0
- package/dist/tui/execute-tool-part-message.js +48 -0
- package/dist/tui/generic-tool-part-message.js +28 -0
- package/dist/tui/index.js +18 -0
- package/dist/tui/input.js +115 -0
- package/dist/tui/markdown.js +31 -0
- package/dist/tui/messages.js +86 -0
- package/dist/tui/spinner-message.js +14 -0
- package/dist/tui/tool-part-message.js +9 -0
- package/dist/tui/use-agent.js +27 -0
- package/dist/tui/use-input-state.js +136 -0
- package/dist/tui/user-part-message.js +9 -0
- package/dist/tui/welcome.js +12 -0
- package/package.json +67 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, writeFile } from "fs/promises";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
5
|
+
const ISSUER = "https://auth.openai.com";
|
|
6
|
+
const CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses";
|
|
7
|
+
const POLL_SAFETY_MS = 3000;
|
|
8
|
+
function parseJwtClaims(token) {
|
|
9
|
+
const parts = token.split(".");
|
|
10
|
+
if (parts.length !== 3)
|
|
11
|
+
return undefined;
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(Buffer.from(parts[1], "base64url").toString());
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function accountIdFromClaims(claims) {
|
|
20
|
+
return (claims.chatgpt_account_id ||
|
|
21
|
+
claims["https://api.openai.com/auth"]?.chatgpt_account_id ||
|
|
22
|
+
claims.organizations?.[0]?.id);
|
|
23
|
+
}
|
|
24
|
+
function extractAccountId(tokens) {
|
|
25
|
+
if (tokens.id_token) {
|
|
26
|
+
const claims = parseJwtClaims(tokens.id_token);
|
|
27
|
+
if (claims) {
|
|
28
|
+
const id = accountIdFromClaims(claims);
|
|
29
|
+
if (id)
|
|
30
|
+
return id;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const claims = parseJwtClaims(tokens.access_token);
|
|
34
|
+
if (!claims)
|
|
35
|
+
return undefined;
|
|
36
|
+
return accountIdFromClaims(claims);
|
|
37
|
+
}
|
|
38
|
+
function expand(p) {
|
|
39
|
+
if (!p.startsWith("~/"))
|
|
40
|
+
return p;
|
|
41
|
+
return path.join(os.homedir(), p.slice(2));
|
|
42
|
+
}
|
|
43
|
+
function buildStore(tokens) {
|
|
44
|
+
return {
|
|
45
|
+
refresh: tokens.refresh_token,
|
|
46
|
+
access: tokens.access_token,
|
|
47
|
+
expires: Date.now() + (tokens.expires_in ?? 3600) * 1000,
|
|
48
|
+
accountId: extractAccountId(tokens),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async function readStore(file) {
|
|
52
|
+
try {
|
|
53
|
+
const raw = await readFile(file, "utf-8");
|
|
54
|
+
return JSON.parse(raw);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function writeStore(file, data) {
|
|
61
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
62
|
+
await writeFile(file, JSON.stringify(data, null, 2), "utf-8");
|
|
63
|
+
await chmod(file, 0o600).catch(() => { });
|
|
64
|
+
}
|
|
65
|
+
function sleep(ms) {
|
|
66
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
67
|
+
}
|
|
68
|
+
function isAllowedSubscriptionHost(hostname) {
|
|
69
|
+
const host = hostname.toLowerCase();
|
|
70
|
+
return (host === "openai.com" ||
|
|
71
|
+
host.endsWith(".openai.com") ||
|
|
72
|
+
host === "chatgpt.com" ||
|
|
73
|
+
host.endsWith(".chatgpt.com"));
|
|
74
|
+
}
|
|
75
|
+
async function refreshToken(baseFetch, refresh) {
|
|
76
|
+
const res = await baseFetch(`${ISSUER}/oauth/token`, {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
79
|
+
body: new URLSearchParams({
|
|
80
|
+
grant_type: "refresh_token",
|
|
81
|
+
refresh_token: refresh,
|
|
82
|
+
client_id: CLIENT_ID,
|
|
83
|
+
}).toString(),
|
|
84
|
+
});
|
|
85
|
+
if (!res.ok) {
|
|
86
|
+
throw new Error(`OpenAI OAuth refresh failed: ${res.status}`);
|
|
87
|
+
}
|
|
88
|
+
return (await res.json());
|
|
89
|
+
}
|
|
90
|
+
async function loginWithDeviceCode(baseFetch) {
|
|
91
|
+
const init = await baseFetch(`${ISSUER}/api/accounts/deviceauth/usercode`, {
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers: { "Content-Type": "application/json" },
|
|
94
|
+
body: JSON.stringify({ client_id: CLIENT_ID }),
|
|
95
|
+
});
|
|
96
|
+
if (!init.ok) {
|
|
97
|
+
throw new Error(`OpenAI OAuth device start failed: ${init.status}`);
|
|
98
|
+
}
|
|
99
|
+
const info = (await init.json());
|
|
100
|
+
const interval = Math.max(parseInt(info.interval) || 5, 1) * 1000;
|
|
101
|
+
const prompt = { url: `${ISSUER}/codex/device`, code: info.user_code };
|
|
102
|
+
console.error(`Open ${prompt.url} and enter code ${prompt.code}`);
|
|
103
|
+
while (true) {
|
|
104
|
+
const poll = await baseFetch(`${ISSUER}/api/accounts/deviceauth/token`, {
|
|
105
|
+
method: "POST",
|
|
106
|
+
headers: { "Content-Type": "application/json" },
|
|
107
|
+
body: JSON.stringify({
|
|
108
|
+
device_auth_id: info.device_auth_id,
|
|
109
|
+
user_code: info.user_code,
|
|
110
|
+
}),
|
|
111
|
+
});
|
|
112
|
+
if (poll.ok) {
|
|
113
|
+
const data = (await poll.json());
|
|
114
|
+
const token = await baseFetch(`${ISSUER}/oauth/token`, {
|
|
115
|
+
method: "POST",
|
|
116
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
117
|
+
body: new URLSearchParams({
|
|
118
|
+
grant_type: "authorization_code",
|
|
119
|
+
code: data.authorization_code,
|
|
120
|
+
redirect_uri: `${ISSUER}/deviceauth/callback`,
|
|
121
|
+
client_id: CLIENT_ID,
|
|
122
|
+
code_verifier: data.code_verifier,
|
|
123
|
+
}).toString(),
|
|
124
|
+
});
|
|
125
|
+
if (!token.ok) {
|
|
126
|
+
throw new Error(`OpenAI OAuth token exchange failed: ${token.status}`);
|
|
127
|
+
}
|
|
128
|
+
return (await token.json());
|
|
129
|
+
}
|
|
130
|
+
if (poll.status !== 403 && poll.status !== 404) {
|
|
131
|
+
throw new Error(`OpenAI OAuth device polling failed: ${poll.status}`);
|
|
132
|
+
}
|
|
133
|
+
await sleep(interval + POLL_SAFETY_MS);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* I don't think this works but leaving it plugged in for now.
|
|
138
|
+
* We might need to set other headers/content in order to use subscriptions.
|
|
139
|
+
*/
|
|
140
|
+
export function createOpenAISubscriptionFetch() {
|
|
141
|
+
const baseFetch = fetch;
|
|
142
|
+
const file = expand("~/.config/ask/openai-oauth.json");
|
|
143
|
+
const skew = 30_000;
|
|
144
|
+
let inflight;
|
|
145
|
+
function isMarked(headers) {
|
|
146
|
+
const value = headers.get("authorization");
|
|
147
|
+
return value === "Bearer oauth";
|
|
148
|
+
}
|
|
149
|
+
function rewriteUrl(url) {
|
|
150
|
+
if (url.pathname.includes("/v1/responses"))
|
|
151
|
+
return new URL(CODEX_API_ENDPOINT);
|
|
152
|
+
if (url.pathname.includes("/chat/completions"))
|
|
153
|
+
return new URL(CODEX_API_ENDPOINT);
|
|
154
|
+
return url;
|
|
155
|
+
}
|
|
156
|
+
async function ensureAuth() {
|
|
157
|
+
if (inflight)
|
|
158
|
+
return inflight;
|
|
159
|
+
const run = (async () => {
|
|
160
|
+
const saved = await readStore(file);
|
|
161
|
+
if (saved && saved.expires > Date.now() + skew) {
|
|
162
|
+
return saved;
|
|
163
|
+
}
|
|
164
|
+
if (saved?.refresh) {
|
|
165
|
+
const next = buildStore(await refreshToken(baseFetch, saved.refresh));
|
|
166
|
+
await writeStore(file, next);
|
|
167
|
+
return next;
|
|
168
|
+
}
|
|
169
|
+
const next = buildStore(await loginWithDeviceCode(baseFetch));
|
|
170
|
+
await writeStore(file, next);
|
|
171
|
+
return next;
|
|
172
|
+
})();
|
|
173
|
+
inflight = run;
|
|
174
|
+
run.finally(() => {
|
|
175
|
+
if (inflight === run)
|
|
176
|
+
inflight = undefined;
|
|
177
|
+
});
|
|
178
|
+
return run;
|
|
179
|
+
}
|
|
180
|
+
return async (input, init) => {
|
|
181
|
+
const req = new Request(input, init);
|
|
182
|
+
if (!isMarked(req.headers)) {
|
|
183
|
+
return baseFetch(input, init);
|
|
184
|
+
}
|
|
185
|
+
const reqUrl = new URL(req.url);
|
|
186
|
+
if (!isAllowedSubscriptionHost(reqUrl.hostname)) {
|
|
187
|
+
throw new Error(`OAuth subscription mode requires an OpenAI host, got: ${reqUrl.hostname}`);
|
|
188
|
+
}
|
|
189
|
+
const auth = await ensureAuth();
|
|
190
|
+
const url = rewriteUrl(reqUrl);
|
|
191
|
+
const out = new Request(url, req);
|
|
192
|
+
out.headers.set("authorization", `Bearer ${auth.access}`);
|
|
193
|
+
if (auth.accountId)
|
|
194
|
+
out.headers.set("ChatGPT-Account-Id", auth.accountId);
|
|
195
|
+
if (!auth.accountId)
|
|
196
|
+
out.headers.delete("ChatGPT-Account-Id");
|
|
197
|
+
return baseFetch(out);
|
|
198
|
+
};
|
|
199
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { join, parse, sep } from 'path';
|
|
2
|
+
export function ancestorPaths(dir) {
|
|
3
|
+
const { root } = parse(dir);
|
|
4
|
+
const parts = dir.slice(root.length).split(sep).filter(Boolean);
|
|
5
|
+
return Array.from({ length: parts.length + 1 }, (_, i) => join(root, ...parts.slice(0, i)));
|
|
6
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export class Serializer {
|
|
2
|
+
tail = Promise.resolve();
|
|
3
|
+
generation = 0;
|
|
4
|
+
async submit(task) {
|
|
5
|
+
const generation = this.generation;
|
|
6
|
+
return this.tail = (async () => {
|
|
7
|
+
try {
|
|
8
|
+
await this.tail;
|
|
9
|
+
}
|
|
10
|
+
catch (ignored) {
|
|
11
|
+
}
|
|
12
|
+
if (this.generation === generation) {
|
|
13
|
+
await task();
|
|
14
|
+
}
|
|
15
|
+
})();
|
|
16
|
+
}
|
|
17
|
+
async cancelPending() {
|
|
18
|
+
this.generation += 1;
|
|
19
|
+
try {
|
|
20
|
+
await this.tail;
|
|
21
|
+
}
|
|
22
|
+
catch (ignored) {
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { appendFile, copyFile, mkdir, readFile, readdir, stat, } from 'fs/promises';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
export class SessionStore {
|
|
7
|
+
sessionId;
|
|
8
|
+
sessionPath;
|
|
9
|
+
constructor(sessionId) {
|
|
10
|
+
this.sessionId = sessionId;
|
|
11
|
+
this.sessionPath = SessionStore.sessionPathFor(sessionId);
|
|
12
|
+
}
|
|
13
|
+
static async create(options) {
|
|
14
|
+
const fallbackLastSessionId = options.resume === true || options.fork !== undefined
|
|
15
|
+
? await SessionStore.lastSessionId()
|
|
16
|
+
: null;
|
|
17
|
+
const sourceSessionId = SessionStore.parseUuid((typeof options.resume === 'string' ? options.resume : undefined) ??
|
|
18
|
+
fallbackLastSessionId ??
|
|
19
|
+
randomUUID());
|
|
20
|
+
const source = new SessionStore(sourceSessionId);
|
|
21
|
+
if (!options.fork)
|
|
22
|
+
return source;
|
|
23
|
+
return source.forked(options.fork === true ? undefined : options.fork);
|
|
24
|
+
}
|
|
25
|
+
static sessionsDir() {
|
|
26
|
+
return join(homedir(), '.ask', 'sessions');
|
|
27
|
+
}
|
|
28
|
+
static sessionPathFor(sessionId) {
|
|
29
|
+
return join(SessionStore.sessionsDir(), `${sessionId}.jsonl`);
|
|
30
|
+
}
|
|
31
|
+
static async lastSessionId() {
|
|
32
|
+
const dir = SessionStore.sessionsDir();
|
|
33
|
+
const entries = (await SessionStore.ignoreMissing(() => readdir(dir))) ?? [];
|
|
34
|
+
const jsonlFiles = entries.filter((f) => f.endsWith('.jsonl'));
|
|
35
|
+
if (jsonlFiles.length === 0)
|
|
36
|
+
return null;
|
|
37
|
+
let latest = null;
|
|
38
|
+
for (const file of jsonlFiles) {
|
|
39
|
+
const s = await stat(join(dir, file));
|
|
40
|
+
if (latest === null || s.mtimeMs > latest.mtimeMs) {
|
|
41
|
+
latest = { id: file.slice(0, -6), mtimeMs: s.mtimeMs };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return latest?.id ?? null;
|
|
45
|
+
}
|
|
46
|
+
static parseUuid(arg) {
|
|
47
|
+
const result = z.uuid().safeParse(arg);
|
|
48
|
+
if (result.success)
|
|
49
|
+
return result.data;
|
|
50
|
+
throw new Error(`invalid session UUID: ${arg}`);
|
|
51
|
+
}
|
|
52
|
+
static async ignoreMissing(op) {
|
|
53
|
+
try {
|
|
54
|
+
return await op();
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (error?.code !== 'ENOENT')
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async read() {
|
|
62
|
+
const content = (await SessionStore.ignoreMissing(() => readFile(this.sessionPath, 'utf-8'))) ?? '';
|
|
63
|
+
return content
|
|
64
|
+
.split('\n')
|
|
65
|
+
.filter((line) => line.length > 0)
|
|
66
|
+
.map((line) => JSON.parse(line));
|
|
67
|
+
}
|
|
68
|
+
async append(messages) {
|
|
69
|
+
await mkdir(SessionStore.sessionsDir(), { recursive: true });
|
|
70
|
+
const lines = messages.map((message) => JSON.stringify(message) + '\n');
|
|
71
|
+
await appendFile(this.sessionPath, lines.join(''), 'utf-8');
|
|
72
|
+
}
|
|
73
|
+
async forked(sessionId) {
|
|
74
|
+
const resolvedSessionId = SessionStore.parseUuid(sessionId ?? randomUUID());
|
|
75
|
+
const forked = new SessionStore(resolvedSessionId);
|
|
76
|
+
await mkdir(SessionStore.sessionsDir(), { recursive: true });
|
|
77
|
+
await SessionStore.ignoreMissing(() => copyFile(this.sessionPath, forked.sessionPath));
|
|
78
|
+
return forked;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { SessionStore, } from './session-store.js';
|
|
3
|
+
export class Session {
|
|
4
|
+
sessionStore;
|
|
5
|
+
messagesById;
|
|
6
|
+
headId = null;
|
|
7
|
+
constructor(messagesById, headId, sessionStore) {
|
|
8
|
+
this.messagesById = messagesById;
|
|
9
|
+
this.headId = headId;
|
|
10
|
+
this.sessionStore = sessionStore;
|
|
11
|
+
}
|
|
12
|
+
static async create(options) {
|
|
13
|
+
const sessionStore = await SessionStore.create(options);
|
|
14
|
+
const messagesById = new Map();
|
|
15
|
+
let headId = null;
|
|
16
|
+
for (const message of await sessionStore.read()) {
|
|
17
|
+
const id = message._meta.id;
|
|
18
|
+
messagesById.set(id, message);
|
|
19
|
+
headId = id;
|
|
20
|
+
}
|
|
21
|
+
return new Session(messagesById, headId, sessionStore);
|
|
22
|
+
}
|
|
23
|
+
get sessionId() {
|
|
24
|
+
return this.sessionStore.sessionId;
|
|
25
|
+
}
|
|
26
|
+
get messages() {
|
|
27
|
+
const messages = [];
|
|
28
|
+
let currentId = this.headId;
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
while (currentId) {
|
|
31
|
+
if (seen.has(currentId)) {
|
|
32
|
+
throw new Error(`cycle detected in message graph at: ${currentId}`);
|
|
33
|
+
}
|
|
34
|
+
seen.add(currentId);
|
|
35
|
+
const message = this.messagesById.get(currentId);
|
|
36
|
+
if (!message)
|
|
37
|
+
throw new Error(`missing message node: ${currentId}`);
|
|
38
|
+
messages.push(message);
|
|
39
|
+
currentId = message._meta.parentId;
|
|
40
|
+
}
|
|
41
|
+
return messages.reverse();
|
|
42
|
+
}
|
|
43
|
+
async append(messages, uiHidden = false) {
|
|
44
|
+
if (messages.length === 0)
|
|
45
|
+
return [];
|
|
46
|
+
const appended = this.withMeta(messages, uiHidden);
|
|
47
|
+
await this.sessionStore.append(appended);
|
|
48
|
+
for (const message of appended) {
|
|
49
|
+
this.messagesById.set(message._meta.id, message);
|
|
50
|
+
}
|
|
51
|
+
const last = appended.at(-1);
|
|
52
|
+
if (last)
|
|
53
|
+
this.headId = last._meta.id;
|
|
54
|
+
return appended;
|
|
55
|
+
}
|
|
56
|
+
async cleared() {
|
|
57
|
+
return new Session(new Map(), null, await SessionStore.create({}));
|
|
58
|
+
}
|
|
59
|
+
async fork(sessionId) {
|
|
60
|
+
this.sessionStore = await this.sessionStore.forked(sessionId);
|
|
61
|
+
}
|
|
62
|
+
rewind(headId) {
|
|
63
|
+
if (headId !== null && !this.messagesById.has(headId)) {
|
|
64
|
+
throw new Error(`unknown message ID: ${headId}`);
|
|
65
|
+
}
|
|
66
|
+
this.headId = headId;
|
|
67
|
+
}
|
|
68
|
+
withMeta(messages, uiHidden) {
|
|
69
|
+
const timestamp = new Date().toISOString();
|
|
70
|
+
const appended = [];
|
|
71
|
+
const initialParent = this.headId;
|
|
72
|
+
for (const [index, message] of messages.entries()) {
|
|
73
|
+
const id = randomUUID();
|
|
74
|
+
const parent = index === 0 ? initialParent : appended[index - 1]._meta.id;
|
|
75
|
+
appended.push({
|
|
76
|
+
...message,
|
|
77
|
+
_meta: {
|
|
78
|
+
id,
|
|
79
|
+
timestamp,
|
|
80
|
+
uiHidden,
|
|
81
|
+
parentId: parent ?? null,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return appended;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { readdir, readFile } from 'fs/promises';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { basename, join, relative, sep } from 'path';
|
|
4
|
+
import { parse } from 'yaml';
|
|
5
|
+
import { ancestorPaths } from './paths.js';
|
|
6
|
+
export class SkillsPrompt {
|
|
7
|
+
async build() {
|
|
8
|
+
const home = homedir();
|
|
9
|
+
const cwd = process.cwd();
|
|
10
|
+
const files = (await Promise.all(this.skillsRoots(home, cwd).map((root) => this.skillFiles(root)))).flat();
|
|
11
|
+
const skills = (await Promise.all(files.map((abs) => this.parseSkill(abs, home, cwd)))).filter((s) => !!s);
|
|
12
|
+
if (!skills.length)
|
|
13
|
+
return '';
|
|
14
|
+
return [
|
|
15
|
+
'The following agent skills have been loaded from SKILL.md files.\n' +
|
|
16
|
+
'Agent skills contain additional instructions/code.\n' +
|
|
17
|
+
'The following are just brief descriptions/summaries indicating relevance.\n' +
|
|
18
|
+
'Read the full SKILL.md file when relevant to your task.',
|
|
19
|
+
'<AGENT_SKILLS>',
|
|
20
|
+
'# Agent skills',
|
|
21
|
+
...skills.map((s) => `## \`${s.name}\`\n\nPath: \`${s.path}\`\n\n${s.description}`),
|
|
22
|
+
'</AGENT_SKILLS>',
|
|
23
|
+
].join('\n\n');
|
|
24
|
+
}
|
|
25
|
+
skillsRoots(home, cwd) {
|
|
26
|
+
return [
|
|
27
|
+
...new Set([home, ...ancestorPaths(cwd)].map((p) => join(p, '.agents', 'skills'))),
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
async skillFiles(skillsRoot) {
|
|
31
|
+
try {
|
|
32
|
+
return (await readdir(skillsRoot, { recursive: true }))
|
|
33
|
+
.filter((f) => /^skill\.md$/i.test(basename(String(f))))
|
|
34
|
+
.map((f) => join(skillsRoot, String(f)));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async parseSkill(abs, home, cwd) {
|
|
41
|
+
try {
|
|
42
|
+
const content = await readFile(abs, 'utf-8');
|
|
43
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
44
|
+
if (!match)
|
|
45
|
+
return;
|
|
46
|
+
const fields = parse(match[1]);
|
|
47
|
+
if (!fields?.name || !fields?.description)
|
|
48
|
+
return;
|
|
49
|
+
const path = abs.startsWith(home + sep)
|
|
50
|
+
? '~' + abs.slice(home.length)
|
|
51
|
+
: relative(cwd, abs);
|
|
52
|
+
return { name: fields.name, path, description: fields.description };
|
|
53
|
+
}
|
|
54
|
+
catch { }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
5
|
+
const __dirname = dirname(__filename);
|
|
6
|
+
export class SystemPrompt {
|
|
7
|
+
build() {
|
|
8
|
+
const promptPath = join(__dirname, '..', '..', 'assets', 'system-prompt.md');
|
|
9
|
+
return readFileSync(promptPath, 'utf-8');
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# System prompt
|
|
2
|
+
|
|
3
|
+
You are an expert coding agent or coding assistant.
|
|
4
|
+
You are typically invoked via the `ask` executable harness.
|
|
5
|
+
The user may refer to you as "Ask".
|
|
6
|
+
You help users with tasks including coding by executing commands
|
|
7
|
+
including those for searching, reading, editing and writing files.
|
|
8
|
+
|
|
9
|
+
Available tools:
|
|
10
|
+
|
|
11
|
+
- `execute`: execute shell commands using bash.
|
|
12
|
+
|
|
13
|
+
Guidelines:
|
|
14
|
+
|
|
15
|
+
- Read relevant files and understand context before making changes.
|
|
16
|
+
- Use `execute` for file operations like `ls`, `rg`, `fd`.
|
|
17
|
+
- When summarizing your actions, output plain text directly - do NOT use cat or bash to display what you did.
|
|
18
|
+
- Be concise in your responses.
|
|
19
|
+
- Show file paths clearly when working with files.
|
|
20
|
+
|
|
21
|
+
## Searching
|
|
22
|
+
|
|
23
|
+
Use `ls`, `rg`, `fd` for exploring the filesystem.
|
|
24
|
+
Use flags for following symlinks, including hidden items,
|
|
25
|
+
and not ignoring ignored items where appropriate,
|
|
26
|
+
eg `ls -a`, `rg -L -uu`, `fd -L -u`.
|
|
27
|
+
|
|
28
|
+
## File editing
|
|
29
|
+
|
|
30
|
+
### Reading
|
|
31
|
+
|
|
32
|
+
Prefer `sed` to `cat` to avoid dumping large files into context.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
sed -n '1,200p' path/to/file
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Writing
|
|
39
|
+
|
|
40
|
+
Use `cat` with heredocs to create new files.
|
|
41
|
+
Check the file doesn't already exist before writing.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
cat > path/to/new-file <<'EOF'
|
|
45
|
+
<new content>
|
|
46
|
+
EOF
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Editing
|
|
50
|
+
|
|
51
|
+
Use `sd -F` with `cat` and heredocs for targeted edits.
|
|
52
|
+
Generally prefer making targeted edits rather than rewriting the entire file.
|
|
53
|
+
After editing, read back the modified region to verify.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
OLD_BLOCK=$(cat <<'OLD_EOF'
|
|
57
|
+
<old content>
|
|
58
|
+
OLD_EOF
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
NEW_BLOCK=$(cat <<'NEW_EOF'
|
|
62
|
+
<new content>
|
|
63
|
+
NEW_EOF
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
sd -F -- "$OLD_BLOCK" "$NEW_BLOCK" path/to/file
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Background processes
|
|
70
|
+
|
|
71
|
+
Use `tmux` when processes need to run in the background.
|
|
72
|
+
For full output, use `capture-pane -S -` or `pipe-pane` to file.
|
|
73
|
+
Prefix session names with "agents-".
|
|
74
|
+
|
|
75
|
+
## Subagents, task delegation, and context management
|
|
76
|
+
|
|
77
|
+
Typically the user has invoked you by running `ask` from a terminal shell.
|
|
78
|
+
You may also run `ask "<msg>"` to invoke another copy of the agent,
|
|
79
|
+
which may be referred to as a subagent.
|
|
80
|
+
You may do this in shell commands or in scripts you execute to help you achieve your tasks.
|
|
81
|
+
However you should not use `ask` in code you generate for the user to run independently.
|
|
82
|
+
The point of delegating is to control the context which you, the main agent, and the subagent sees.
|
|
83
|
+
If you need to answer a complicated query in the middle of a conversation, by delegating to a subagent,
|
|
84
|
+
the subagent does not see the unnecessary full context of your conversation.
|
|
85
|
+
It only sees what you explicitly include in the prompt.
|
|
86
|
+
Similarly, you do not see the output of the intermediate steps the subagent used to answer the question.
|
|
87
|
+
By keeping the context of yourself and subagents limited to only the scope they require,
|
|
88
|
+
overall accuracy is typically improved.
|
|
89
|
+
Also, by invoking multiple subagents in a single turn, you can achieve parallelism
|
|
90
|
+
and therefore faster performance for tasks that are truly independent.
|
|
91
|
+
|
|
92
|
+
## Session storage
|
|
93
|
+
|
|
94
|
+
When `ask` runs, messages are persisted to `~/.ask/sessions/<id>.jsonl` as AI SDK messages.
|
|
95
|
+
IDs are UUIDs.
|
|
96
|
+
Messages include additional metadata in a `_meta` property, including `id`, `parentId`, `uiHidden`, and `timestamp`.
|
|
97
|
+
When using subagents, consider passing an explicit session ID via `ask --resume <id> ...`.
|
|
98
|
+
This allows you to inspect the context of the subagent, though note that
|
|
99
|
+
usually you explicitly don't want the subagent context in your own.
|
|
100
|
+
This also allows you ask a subagent follow up question by supplying the same session ID twice.
|
|
101
|
+
It may be useful to make focused searches of the subagent context.
|
|
102
|
+
|
|
103
|
+
## Web
|
|
104
|
+
|
|
105
|
+
For searching or fetching from the web, you may delegate to `codex`, another coding agent.
|
|
106
|
+
|
|
107
|
+
codex exec --skip-git-repo-check "What is the weather like in London today?"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ExecuteTool } from './execute-tool.js';
|
|
2
|
+
export class Tools {
|
|
3
|
+
executeTool = new ExecuteTool();
|
|
4
|
+
definitions() {
|
|
5
|
+
return {
|
|
6
|
+
[this.executeTool.name]: this.executeTool.definition(),
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
execute(name, args, signal) {
|
|
10
|
+
switch (name) {
|
|
11
|
+
case this.executeTool.name:
|
|
12
|
+
return this.executeTool.execute(args, signal);
|
|
13
|
+
default:
|
|
14
|
+
throw new Error(`unknown tool: ${name}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|