@poncho-ai/harness 0.37.1 → 0.38.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/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +32 -0
- package/dist/index.d.ts +66 -34
- package/dist/index.js +382 -59
- package/package.json +1 -1
- package/src/harness.ts +63 -13
- package/src/prompt-cache.ts +13 -4
- package/src/reminder-store.ts +183 -16
- package/src/reminder-tools.ts +102 -6
- package/src/state.ts +29 -3
- package/src/storage/engine.ts +10 -10
- package/src/storage/memory-engine.ts +25 -10
- package/src/storage/schema.ts +11 -0
- package/src/storage/sql-dialect.ts +80 -15
- package/src/storage/store-adapters.ts +11 -11
- package/src/vfs/bash-manager.ts +16 -2
- package/src/vfs/edit-file-tool.ts +9 -8
- package/src/vfs/read-file-tool.ts +14 -15
- package/src/vfs/write-file-tool.ts +6 -5
- package/test/harness.test.ts +1 -1
- package/test/reminder-store.test.ts +193 -4
- package/test/storage-engine.test.ts +25 -0
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import type {
|
|
7
7
|
Conversation,
|
|
8
|
+
ConversationCreateInit,
|
|
8
9
|
ConversationSummary,
|
|
9
10
|
PendingSubagentResult,
|
|
10
11
|
} from "../state.js";
|
|
11
12
|
import type { MainMemory } from "../memory.js";
|
|
12
13
|
import type { TodoItem } from "../todo-tools.js";
|
|
13
|
-
import type { Reminder } from "../reminder-store.js";
|
|
14
|
+
import type { Reminder, ReminderCreateInput, ReminderStatus } from "../reminder-store.js";
|
|
14
15
|
import type { StorageEngine, VfsDirEntry, VfsStat } from "./engine.js";
|
|
15
16
|
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
@@ -105,16 +106,26 @@ export class InMemoryEngine implements StorageEngine {
|
|
|
105
106
|
ownerId?: string,
|
|
106
107
|
title?: string,
|
|
107
108
|
tenantId?: string | null,
|
|
109
|
+
init?: ConversationCreateInit,
|
|
108
110
|
): Promise<Conversation> => {
|
|
109
111
|
const now = Date.now();
|
|
110
112
|
const conv: Conversation = {
|
|
111
113
|
conversationId: randomUUID(),
|
|
112
114
|
title: normalizeTitle(title),
|
|
113
|
-
messages: [],
|
|
115
|
+
messages: init?.messages ?? [],
|
|
114
116
|
ownerId: ownerId ?? DEFAULT_OWNER,
|
|
115
117
|
tenantId: tenantId === undefined ? null : tenantId,
|
|
116
118
|
createdAt: now,
|
|
117
119
|
updatedAt: now,
|
|
120
|
+
...(init?.parentConversationId !== undefined
|
|
121
|
+
? { parentConversationId: init.parentConversationId }
|
|
122
|
+
: {}),
|
|
123
|
+
...(init?.subagentMeta !== undefined
|
|
124
|
+
? { subagentMeta: init.subagentMeta }
|
|
125
|
+
: {}),
|
|
126
|
+
...(init?.channelMeta !== undefined
|
|
127
|
+
? { channelMeta: init.channelMeta }
|
|
128
|
+
: {}),
|
|
118
129
|
};
|
|
119
130
|
this.convs.set(conv.conversationId, conv);
|
|
120
131
|
return conv;
|
|
@@ -237,14 +248,7 @@ export class InMemoryEngine implements StorageEngine {
|
|
|
237
248
|
return results;
|
|
238
249
|
},
|
|
239
250
|
|
|
240
|
-
create: async (input: {
|
|
241
|
-
task: string;
|
|
242
|
-
scheduledAt: number;
|
|
243
|
-
timezone?: string;
|
|
244
|
-
conversationId: string;
|
|
245
|
-
ownerId?: string;
|
|
246
|
-
tenantId?: string | null;
|
|
247
|
-
}): Promise<Reminder> => {
|
|
251
|
+
create: async (input: ReminderCreateInput): Promise<Reminder> => {
|
|
248
252
|
const r: Reminder = {
|
|
249
253
|
id: randomUUID(),
|
|
250
254
|
task: input.task,
|
|
@@ -255,11 +259,22 @@ export class InMemoryEngine implements StorageEngine {
|
|
|
255
259
|
conversationId: input.conversationId,
|
|
256
260
|
ownerId: input.ownerId,
|
|
257
261
|
tenantId: input.tenantId,
|
|
262
|
+
recurrence: input.recurrence ?? null,
|
|
263
|
+
occurrenceCount: 0,
|
|
258
264
|
};
|
|
259
265
|
this.reminderData.set(r.id, r);
|
|
260
266
|
return r;
|
|
261
267
|
},
|
|
262
268
|
|
|
269
|
+
update: async (id: string, fields: { scheduledAt?: number; occurrenceCount?: number; status?: ReminderStatus }): Promise<Reminder> => {
|
|
270
|
+
const r = this.reminderData.get(id);
|
|
271
|
+
if (!r) throw new Error(`Reminder ${id} not found`);
|
|
272
|
+
if (fields.scheduledAt !== undefined) r.scheduledAt = fields.scheduledAt;
|
|
273
|
+
if (fields.occurrenceCount !== undefined) r.occurrenceCount = fields.occurrenceCount;
|
|
274
|
+
if (fields.status !== undefined) r.status = fields.status;
|
|
275
|
+
return r;
|
|
276
|
+
},
|
|
277
|
+
|
|
263
278
|
cancel: async (id: string): Promise<Reminder> => {
|
|
264
279
|
const r = this.reminderData.get(id);
|
|
265
280
|
if (!r) throw new Error(`Reminder ${id} not found`);
|
package/src/storage/schema.ts
CHANGED
|
@@ -174,4 +174,15 @@ export const migrations: Migration[] = [
|
|
|
174
174
|
];
|
|
175
175
|
},
|
|
176
176
|
},
|
|
177
|
+
{
|
|
178
|
+
version: 5,
|
|
179
|
+
name: "add_reminder_recurrence",
|
|
180
|
+
up: (d) => {
|
|
181
|
+
const jsonType = d === "sqlite" ? "TEXT" : "JSONB";
|
|
182
|
+
return [
|
|
183
|
+
`ALTER TABLE reminders ADD COLUMN recurrence ${jsonType}`,
|
|
184
|
+
`ALTER TABLE reminders ADD COLUMN occurrence_count INTEGER NOT NULL DEFAULT 0`,
|
|
185
|
+
];
|
|
186
|
+
},
|
|
187
|
+
},
|
|
177
188
|
];
|
|
@@ -10,12 +10,13 @@
|
|
|
10
10
|
import { randomUUID } from "node:crypto";
|
|
11
11
|
import type {
|
|
12
12
|
Conversation,
|
|
13
|
+
ConversationCreateInit,
|
|
13
14
|
ConversationSummary,
|
|
14
15
|
PendingSubagentResult,
|
|
15
16
|
} from "../state.js";
|
|
16
17
|
import type { MainMemory } from "../memory.js";
|
|
17
18
|
import type { TodoItem } from "../todo-tools.js";
|
|
18
|
-
import type { Reminder } from "../reminder-store.js";
|
|
19
|
+
import type { Reminder, ReminderCreateInput, ReminderStatus } from "../reminder-store.js";
|
|
19
20
|
import type { StorageEngine, VfsDirEntry, VfsStat } from "./engine.js";
|
|
20
21
|
import { type DialectTag, migrations } from "./schema.js";
|
|
21
22
|
|
|
@@ -244,19 +245,30 @@ export abstract class SqlStorageEngine implements StorageEngine {
|
|
|
244
245
|
ownerId?: string,
|
|
245
246
|
title?: string,
|
|
246
247
|
tenantId?: string | null,
|
|
248
|
+
init?: ConversationCreateInit,
|
|
247
249
|
): Promise<Conversation> => {
|
|
248
250
|
const id = randomUUID();
|
|
249
251
|
const now = Date.now();
|
|
250
252
|
const conv: Conversation = {
|
|
251
253
|
conversationId: id,
|
|
252
254
|
title: normalizeTitle(title),
|
|
253
|
-
messages: [],
|
|
255
|
+
messages: init?.messages ?? [],
|
|
254
256
|
ownerId: ownerId ?? DEFAULT_OWNER,
|
|
255
257
|
tenantId: tenantId === undefined ? null : tenantId,
|
|
256
258
|
createdAt: now,
|
|
257
259
|
updatedAt: now,
|
|
260
|
+
...(init?.parentConversationId !== undefined
|
|
261
|
+
? { parentConversationId: init.parentConversationId }
|
|
262
|
+
: {}),
|
|
263
|
+
...(init?.subagentMeta !== undefined
|
|
264
|
+
? { subagentMeta: init.subagentMeta }
|
|
265
|
+
: {}),
|
|
266
|
+
...(init?.channelMeta !== undefined
|
|
267
|
+
? { channelMeta: init.channelMeta }
|
|
268
|
+
: {}),
|
|
258
269
|
};
|
|
259
270
|
const data = JSON.stringify(conv);
|
|
271
|
+
const channelMetaJson = conv.channelMeta ? JSON.stringify(conv.channelMeta) : null;
|
|
260
272
|
await this.executor.run(
|
|
261
273
|
rewrite(
|
|
262
274
|
`INSERT INTO conversations (id, agent_id, tenant_id, owner_id, title, data, message_count, created_at, updated_at,
|
|
@@ -271,12 +283,12 @@ export abstract class SqlStorageEngine implements StorageEngine {
|
|
|
271
283
|
conv.ownerId,
|
|
272
284
|
conv.title,
|
|
273
285
|
data,
|
|
274
|
-
|
|
286
|
+
conv.messages.length,
|
|
275
287
|
new Date(now).toISOString(),
|
|
276
288
|
new Date(now).toISOString(),
|
|
277
|
-
null,
|
|
289
|
+
conv.parentConversationId ?? null,
|
|
278
290
|
0,
|
|
279
|
-
|
|
291
|
+
channelMetaJson,
|
|
280
292
|
],
|
|
281
293
|
);
|
|
282
294
|
return conv;
|
|
@@ -497,21 +509,15 @@ export abstract class SqlStorageEngine implements StorageEngine {
|
|
|
497
509
|
return rows.map((r) => this.rowToReminder(r));
|
|
498
510
|
},
|
|
499
511
|
|
|
500
|
-
create: async (input: {
|
|
501
|
-
task: string;
|
|
502
|
-
scheduledAt: number;
|
|
503
|
-
timezone?: string;
|
|
504
|
-
conversationId: string;
|
|
505
|
-
ownerId?: string;
|
|
506
|
-
tenantId?: string | null;
|
|
507
|
-
}): Promise<Reminder> => {
|
|
512
|
+
create: async (input: ReminderCreateInput): Promise<Reminder> => {
|
|
508
513
|
const id = randomUUID();
|
|
509
514
|
const now = Date.now();
|
|
510
515
|
const tid = normalizeTenant(input.tenantId);
|
|
516
|
+
const recurrenceJson = input.recurrence ? JSON.stringify(input.recurrence) : null;
|
|
511
517
|
await this.executor.run(
|
|
512
518
|
rewrite(
|
|
513
|
-
`INSERT INTO reminders (id, agent_id, tenant_id, owner_id, conversation_id, task, status, scheduled_at, timezone, created_at)
|
|
514
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
|
|
519
|
+
`INSERT INTO reminders (id, agent_id, tenant_id, owner_id, conversation_id, task, status, scheduled_at, timezone, created_at, recurrence, occurrence_count)
|
|
520
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`,
|
|
515
521
|
this.dialect,
|
|
516
522
|
),
|
|
517
523
|
[
|
|
@@ -525,6 +531,8 @@ export abstract class SqlStorageEngine implements StorageEngine {
|
|
|
525
531
|
input.scheduledAt,
|
|
526
532
|
input.timezone ?? null,
|
|
527
533
|
new Date(now).toISOString(),
|
|
534
|
+
recurrenceJson,
|
|
535
|
+
0,
|
|
528
536
|
],
|
|
529
537
|
);
|
|
530
538
|
return {
|
|
@@ -537,9 +545,54 @@ export abstract class SqlStorageEngine implements StorageEngine {
|
|
|
537
545
|
conversationId: input.conversationId,
|
|
538
546
|
ownerId: input.ownerId,
|
|
539
547
|
tenantId: input.tenantId,
|
|
548
|
+
recurrence: input.recurrence ?? null,
|
|
549
|
+
occurrenceCount: 0,
|
|
540
550
|
};
|
|
541
551
|
},
|
|
542
552
|
|
|
553
|
+
update: async (id: string, fields: { scheduledAt?: number; occurrenceCount?: number; status?: ReminderStatus }): Promise<Reminder> => {
|
|
554
|
+
const setClauses: string[] = [];
|
|
555
|
+
const params: unknown[] = [];
|
|
556
|
+
let idx = 1;
|
|
557
|
+
if (fields.scheduledAt !== undefined) {
|
|
558
|
+
setClauses.push(`scheduled_at = $${idx++}`);
|
|
559
|
+
params.push(fields.scheduledAt);
|
|
560
|
+
}
|
|
561
|
+
if (fields.occurrenceCount !== undefined) {
|
|
562
|
+
setClauses.push(`occurrence_count = $${idx++}`);
|
|
563
|
+
params.push(fields.occurrenceCount);
|
|
564
|
+
}
|
|
565
|
+
if (fields.status !== undefined) {
|
|
566
|
+
setClauses.push(`status = $${idx++}`);
|
|
567
|
+
params.push(fields.status);
|
|
568
|
+
}
|
|
569
|
+
if (setClauses.length === 0) {
|
|
570
|
+
// Nothing to update — just fetch
|
|
571
|
+
const row = await this.executor.get(
|
|
572
|
+
rewrite("SELECT * FROM reminders WHERE id = $1 AND agent_id = $2", this.dialect),
|
|
573
|
+
[id, this.agentId],
|
|
574
|
+
);
|
|
575
|
+
if (!row) throw new Error(`Reminder ${id} not found`);
|
|
576
|
+
return this.rowToReminder(row);
|
|
577
|
+
}
|
|
578
|
+
params.push(id, this.agentId);
|
|
579
|
+
const idIdx = idx++;
|
|
580
|
+
const agentIdx = idx++;
|
|
581
|
+
await this.executor.run(
|
|
582
|
+
rewrite(
|
|
583
|
+
`UPDATE reminders SET ${setClauses.join(", ")} WHERE id = $${idIdx} AND agent_id = $${agentIdx}`,
|
|
584
|
+
this.dialect,
|
|
585
|
+
),
|
|
586
|
+
params,
|
|
587
|
+
);
|
|
588
|
+
const row = await this.executor.get(
|
|
589
|
+
rewrite("SELECT * FROM reminders WHERE id = $1 AND agent_id = $2", this.dialect),
|
|
590
|
+
[id, this.agentId],
|
|
591
|
+
);
|
|
592
|
+
if (!row) throw new Error(`Reminder ${id} not found`);
|
|
593
|
+
return this.rowToReminder(row);
|
|
594
|
+
},
|
|
595
|
+
|
|
543
596
|
cancel: async (id: string): Promise<Reminder> => {
|
|
544
597
|
await this.executor.run(
|
|
545
598
|
rewrite(
|
|
@@ -918,6 +971,16 @@ export abstract class SqlStorageEngine implements StorageEngine {
|
|
|
918
971
|
|
|
919
972
|
private rowToReminder(row: QueryRow): Reminder {
|
|
920
973
|
const tid = row.tenant_id as string;
|
|
974
|
+
let recurrence: Reminder["recurrence"] = null;
|
|
975
|
+
if (row.recurrence) {
|
|
976
|
+
try {
|
|
977
|
+
recurrence = typeof row.recurrence === "string"
|
|
978
|
+
? JSON.parse(row.recurrence)
|
|
979
|
+
: row.recurrence;
|
|
980
|
+
} catch {
|
|
981
|
+
recurrence = null;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
921
984
|
return {
|
|
922
985
|
id: row.id as string,
|
|
923
986
|
task: row.task as string,
|
|
@@ -928,6 +991,8 @@ export abstract class SqlStorageEngine implements StorageEngine {
|
|
|
928
991
|
conversationId: row.conversation_id as string,
|
|
929
992
|
ownerId: (row.owner_id as string) ?? undefined,
|
|
930
993
|
tenantId: tid === DEFAULT_TENANT ? null : tid,
|
|
994
|
+
recurrence,
|
|
995
|
+
occurrenceCount: (row.occurrence_count as number) ?? 0,
|
|
931
996
|
};
|
|
932
997
|
}
|
|
933
998
|
|
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
|
|
7
7
|
import type {
|
|
8
8
|
Conversation,
|
|
9
|
+
ConversationCreateInit,
|
|
9
10
|
ConversationStore,
|
|
10
11
|
ConversationSummary,
|
|
11
12
|
PendingSubagentResult,
|
|
12
13
|
} from "../state.js";
|
|
13
14
|
import type { MainMemory, MemoryStore } from "../memory.js";
|
|
14
15
|
import type { TodoItem, TodoStore } from "../todo-tools.js";
|
|
15
|
-
import type { Reminder, ReminderStore } from "../reminder-store.js";
|
|
16
|
+
import type { Reminder, ReminderCreateInput, ReminderStatus, ReminderStore } from "../reminder-store.js";
|
|
16
17
|
import type { StorageEngine } from "./engine.js";
|
|
17
18
|
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
@@ -35,8 +36,12 @@ export function createConversationStoreFromEngine(
|
|
|
35
36
|
engine.conversations.list(ownerId, tenantId),
|
|
36
37
|
get: (conversationId: string) =>
|
|
37
38
|
engine.conversations.get(conversationId),
|
|
38
|
-
create: (
|
|
39
|
-
|
|
39
|
+
create: (
|
|
40
|
+
ownerId?: string,
|
|
41
|
+
title?: string,
|
|
42
|
+
tenantId?: string | null,
|
|
43
|
+
init?: ConversationCreateInit,
|
|
44
|
+
) => engine.conversations.create(ownerId, title, tenantId, init),
|
|
40
45
|
update: (conversation: Conversation) =>
|
|
41
46
|
engine.conversations.update(conversation),
|
|
42
47
|
rename: (conversationId: string, title: string) =>
|
|
@@ -86,14 +91,9 @@ export function createReminderStoreFromEngine(
|
|
|
86
91
|
): ReminderStore {
|
|
87
92
|
return {
|
|
88
93
|
list: () => engine.reminders.list(),
|
|
89
|
-
create: (input:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
timezone?: string;
|
|
93
|
-
conversationId: string;
|
|
94
|
-
ownerId?: string;
|
|
95
|
-
tenantId?: string | null;
|
|
96
|
-
}) => engine.reminders.create(input),
|
|
94
|
+
create: (input: ReminderCreateInput) => engine.reminders.create(input),
|
|
95
|
+
update: (id: string, fields: { scheduledAt?: number; occurrenceCount?: number; status?: ReminderStatus }) =>
|
|
96
|
+
engine.reminders.update(id, fields),
|
|
97
97
|
cancel: (id: string) => engine.reminders.cancel(id),
|
|
98
98
|
delete: (id: string) => engine.reminders.delete(id),
|
|
99
99
|
};
|
package/src/vfs/bash-manager.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { Bash } from "just-bash";
|
|
|
6
6
|
import type {
|
|
7
7
|
BashOptions,
|
|
8
8
|
CommandName,
|
|
9
|
+
IFileSystem,
|
|
9
10
|
NetworkConfig as JustBashNetworkConfig,
|
|
10
11
|
} from "just-bash";
|
|
11
12
|
import type { StorageEngine } from "../storage/engine.js";
|
|
@@ -65,6 +66,7 @@ function toBashOptions(
|
|
|
65
66
|
|
|
66
67
|
export class BashEnvironmentManager {
|
|
67
68
|
private environments = new Map<string, Bash>();
|
|
69
|
+
private filesystems = new Map<string, IFileSystem>();
|
|
68
70
|
private readonly workingDir: string | null;
|
|
69
71
|
private readonly bashOptions: Partial<BashOptions>;
|
|
70
72
|
|
|
@@ -79,11 +81,21 @@ export class BashEnvironmentManager {
|
|
|
79
81
|
this.bashOptions = toBashOptions(bashConfig, network);
|
|
80
82
|
}
|
|
81
83
|
|
|
84
|
+
/** Return the combined IFileSystem (VFS + optional /project mount) for a tenant. */
|
|
85
|
+
getFs(tenantId: string): IFileSystem {
|
|
86
|
+
let fs = this.filesystems.get(tenantId);
|
|
87
|
+
if (!fs) {
|
|
88
|
+
const adapter = new PonchoFsAdapter(this.engine, tenantId, this.limits);
|
|
89
|
+
fs = createBashFs(adapter, this.workingDir);
|
|
90
|
+
this.filesystems.set(tenantId, fs);
|
|
91
|
+
}
|
|
92
|
+
return fs;
|
|
93
|
+
}
|
|
94
|
+
|
|
82
95
|
getOrCreate(tenantId: string): Bash {
|
|
83
96
|
let bash = this.environments.get(tenantId);
|
|
84
97
|
if (!bash) {
|
|
85
|
-
const
|
|
86
|
-
const fs = createBashFs(adapter, this.workingDir);
|
|
98
|
+
const fs = this.getFs(tenantId);
|
|
87
99
|
bash = new Bash({
|
|
88
100
|
fs,
|
|
89
101
|
cwd: "/",
|
|
@@ -112,9 +124,11 @@ export class BashEnvironmentManager {
|
|
|
112
124
|
|
|
113
125
|
destroy(tenantId: string): void {
|
|
114
126
|
this.environments.delete(tenantId);
|
|
127
|
+
this.filesystems.delete(tenantId);
|
|
115
128
|
}
|
|
116
129
|
|
|
117
130
|
destroyAll(): void {
|
|
118
131
|
this.environments.clear();
|
|
132
|
+
this.filesystems.clear();
|
|
119
133
|
}
|
|
120
134
|
}
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
4
|
|
|
5
5
|
import { defineTool, type ToolDefinition } from "@poncho-ai/sdk";
|
|
6
|
-
import type {
|
|
6
|
+
import type { IFileSystem } from "just-bash";
|
|
7
7
|
|
|
8
8
|
export const createEditFileTool = (
|
|
9
|
-
|
|
9
|
+
getFs: (tenantId: string) => IFileSystem,
|
|
10
10
|
): ToolDefinition => defineTool({
|
|
11
11
|
name: "edit_file",
|
|
12
12
|
description:
|
|
@@ -44,12 +44,13 @@ export const createEditFileTool = (
|
|
|
44
44
|
if (!oldStr) throw new Error("old_str must not be empty");
|
|
45
45
|
|
|
46
46
|
const tenantId = context.tenantId ?? "__default__";
|
|
47
|
-
const
|
|
48
|
-
if (!stat) throw new Error(`File not found: ${filePath}`);
|
|
49
|
-
if (stat.type === "directory") throw new Error(`${filePath} is a directory`);
|
|
47
|
+
const fs = getFs(tenantId);
|
|
50
48
|
|
|
51
|
-
|
|
52
|
-
const
|
|
49
|
+
if (!(await fs.exists(filePath))) throw new Error(`File not found: ${filePath}`);
|
|
50
|
+
const stat = await fs.stat(filePath);
|
|
51
|
+
if (stat.isDirectory) throw new Error(`${filePath} is a directory`);
|
|
52
|
+
|
|
53
|
+
const content = await fs.readFile(filePath);
|
|
53
54
|
|
|
54
55
|
const first = content.indexOf(oldStr);
|
|
55
56
|
if (first === -1) {
|
|
@@ -65,7 +66,7 @@ export const createEditFileTool = (
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
const updated = content.slice(0, first) + newStr + content.slice(first + oldStr.length);
|
|
68
|
-
await
|
|
69
|
+
await fs.writeFile(filePath, updated);
|
|
69
70
|
|
|
70
71
|
return { ok: true, path: filePath };
|
|
71
72
|
},
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
|
-
// read_file tool – read files from the
|
|
3
|
-
// PDFs) as
|
|
4
|
-
// model-request time via the vfs:// scheme.
|
|
2
|
+
// read_file tool – read files from the filesystem, returning binary files
|
|
3
|
+
// (images, PDFs) as inline base64 media parts.
|
|
5
4
|
// ---------------------------------------------------------------------------
|
|
6
5
|
|
|
7
6
|
import { defineTool, type ToolDefinition } from "@poncho-ai/sdk";
|
|
8
|
-
import type {
|
|
9
|
-
import { VFS_SCHEME } from "../upload-store.js";
|
|
7
|
+
import type { IFileSystem } from "just-bash";
|
|
10
8
|
|
|
11
9
|
const MIME_MAP: Record<string, string> = {
|
|
12
10
|
".txt": "text/plain",
|
|
@@ -48,7 +46,7 @@ const isTextMime = (mime: string): boolean =>
|
|
|
48
46
|
mime === "application/x-sh";
|
|
49
47
|
|
|
50
48
|
export const createReadFileTool = (
|
|
51
|
-
|
|
49
|
+
getFs: (tenantId: string) => IFileSystem,
|
|
52
50
|
): ToolDefinition => defineTool({
|
|
53
51
|
name: "read_file",
|
|
54
52
|
description:
|
|
@@ -73,29 +71,30 @@ export const createReadFileTool = (
|
|
|
73
71
|
}
|
|
74
72
|
|
|
75
73
|
const tenantId = context.tenantId ?? "__default__";
|
|
76
|
-
const
|
|
77
|
-
|
|
74
|
+
const fs = getFs(tenantId);
|
|
75
|
+
|
|
76
|
+
if (!(await fs.exists(filePath))) {
|
|
78
77
|
throw new Error(`File not found: ${filePath}`);
|
|
79
78
|
}
|
|
80
|
-
|
|
79
|
+
const stat = await fs.stat(filePath);
|
|
80
|
+
if (stat.isDirectory) {
|
|
81
81
|
throw new Error(`${filePath} is a directory, not a file`);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
const mediaType =
|
|
84
|
+
const mediaType = mimeFromPath(filePath) ?? "application/octet-stream";
|
|
85
85
|
const filename = filePath.split("/").pop() ?? filePath;
|
|
86
86
|
|
|
87
87
|
// Text files: read and return inline
|
|
88
88
|
if (isTextMime(mediaType)) {
|
|
89
|
-
const
|
|
90
|
-
const text = Buffer.from(buf).toString("utf8");
|
|
89
|
+
const text = await fs.readFile(filePath);
|
|
91
90
|
return { filename, mediaType, content: text };
|
|
92
91
|
}
|
|
93
92
|
|
|
94
|
-
//
|
|
95
|
-
|
|
93
|
+
// Binary files (images, PDFs): read bytes and return as base64
|
|
94
|
+
const buf = await fs.readFileBuffer(filePath);
|
|
96
95
|
return {
|
|
97
96
|
type: "file",
|
|
98
|
-
data:
|
|
97
|
+
data: Buffer.from(buf).toString("base64"),
|
|
99
98
|
mediaType,
|
|
100
99
|
filename,
|
|
101
100
|
};
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
|
-
// write_file tool – create or overwrite a file in the
|
|
2
|
+
// write_file tool – create or overwrite a file in the filesystem.
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
4
|
|
|
5
5
|
import { defineTool, type ToolDefinition } from "@poncho-ai/sdk";
|
|
6
|
-
import type {
|
|
6
|
+
import type { IFileSystem } from "just-bash";
|
|
7
7
|
|
|
8
8
|
export const createWriteFileTool = (
|
|
9
|
-
|
|
9
|
+
getFs: (tenantId: string) => IFileSystem,
|
|
10
10
|
): ToolDefinition => defineTool({
|
|
11
11
|
name: "write_file",
|
|
12
12
|
description:
|
|
@@ -35,14 +35,15 @@ export const createWriteFileTool = (
|
|
|
35
35
|
if (!filePath) throw new Error("path is required");
|
|
36
36
|
|
|
37
37
|
const tenantId = context.tenantId ?? "__default__";
|
|
38
|
+
const fs = getFs(tenantId);
|
|
38
39
|
|
|
39
40
|
// Create parent directories
|
|
40
41
|
const dir = filePath.slice(0, filePath.lastIndexOf("/"));
|
|
41
42
|
if (dir) {
|
|
42
|
-
await
|
|
43
|
+
await fs.mkdir(dir, { recursive: true });
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
await
|
|
46
|
+
await fs.writeFile(filePath, content);
|
|
46
47
|
|
|
47
48
|
return { ok: true, path: filePath };
|
|
48
49
|
},
|
package/test/harness.test.ts
CHANGED
|
@@ -617,7 +617,7 @@ description: Safe skill
|
|
|
617
617
|
script: "../outside.ts",
|
|
618
618
|
}, stubContext);
|
|
619
619
|
expect(result).toMatchObject({
|
|
620
|
-
error: expect.stringContaining("
|
|
620
|
+
error: expect.stringContaining("Expected a relative path"),
|
|
621
621
|
});
|
|
622
622
|
});
|
|
623
623
|
|