@listen1954/board 0.1.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 +29 -0
- package/client.d.ts +2 -0
- package/cordis.patch.yml +3 -0
- package/lib/client.js +2374 -0
- package/lib/domain.d.ts +213 -0
- package/lib/domain.js +669 -0
- package/lib/index.d.ts +13 -0
- package/lib/index.js +56 -0
- package/lib/prompt.d.ts +2 -0
- package/lib/prompt.js +18 -0
- package/lib/rpc.d.ts +21 -0
- package/lib/rpc.js +101 -0
- package/lib/service.d.ts +74 -0
- package/lib/service.js +417 -0
- package/lib/store.d.ts +22 -0
- package/lib/store.js +178 -0
- package/lib/tools.d.ts +23 -0
- package/lib/tools.js +594 -0
- package/package.json +63 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
2
|
+
import { createBoardService } from './service.js';
|
|
3
|
+
import { BoardStore, resolveBoardRoot } from './store.js';
|
|
4
|
+
import { registerBoardTools } from './tools.js';
|
|
5
|
+
import { registerBoardRpc } from './rpc.js';
|
|
6
|
+
import { BOARD_SYSTEM_PROMPT } from './prompt.js';
|
|
7
|
+
export const name = 'board';
|
|
8
|
+
export const inject = ['tools'];
|
|
9
|
+
export const Config = Schema.object({
|
|
10
|
+
wakeOnGlobalReminder: Schema.boolean().default(false),
|
|
11
|
+
});
|
|
12
|
+
export function apply(ctx, config = { wakeOnGlobalReminder: false }) {
|
|
13
|
+
const store = new BoardStore(resolveBoardRoot());
|
|
14
|
+
const service = createBoardService(ctx, store, {
|
|
15
|
+
wakeOnGlobalReminder: config.wakeOnGlobalReminder ?? false,
|
|
16
|
+
});
|
|
17
|
+
registerBoardRpc(ctx, service);
|
|
18
|
+
registerBoardTools(ctx);
|
|
19
|
+
ctx.effect(() => {
|
|
20
|
+
let running = false;
|
|
21
|
+
const scan = async () => {
|
|
22
|
+
if (running)
|
|
23
|
+
return;
|
|
24
|
+
running = true;
|
|
25
|
+
try {
|
|
26
|
+
const events = await service.fireDue();
|
|
27
|
+
for (const event of events) {
|
|
28
|
+
for (const reminder of event.fired) {
|
|
29
|
+
ctx.logger.info(`[board] reminder fired: ${reminder.title} (${event.scope})`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
ctx.logger.error(`[board] reminder scan failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
running = false;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const timer = setInterval(() => { void scan(); }, 3000);
|
|
41
|
+
timer.unref();
|
|
42
|
+
void scan();
|
|
43
|
+
return () => clearInterval(timer);
|
|
44
|
+
}, 'board: reminder scheduler');
|
|
45
|
+
ctx.inject(['systemPrompt'], (promptCtx) => {
|
|
46
|
+
promptCtx.systemPrompt.section({
|
|
47
|
+
name: 'board',
|
|
48
|
+
order: 210,
|
|
49
|
+
text: BOARD_SYSTEM_PROMPT,
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
ctx.logger.info('[board] ready');
|
|
53
|
+
}
|
|
54
|
+
export { BoardNotFoundError, BoardOpError, BoardRevisionError } from './domain.js';
|
|
55
|
+
export { BoardModel, BoardService } from './service.js';
|
|
56
|
+
export { BoardStore, resolveBoardRoot, resolveDshHome } from './store.js';
|
package/lib/prompt.d.ts
ADDED
package/lib/prompt.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Short system-prompt section. Card bodies stay out of the prompt. */
|
|
2
|
+
export const BOARD_SYSTEM_PROMPT = [
|
|
3
|
+
'You have one unified Board. The global view contains all normal and session-linked columns; session scope projects only this conversation\'s task column.',
|
|
4
|
+
'An agent in any conversation may use global scope to read and modify the entire board. Use session scope only when the user explicitly means this conversation or the new task clearly belongs to it.',
|
|
5
|
+
'Choose tools by intent: board_list is only for an overview of both global and current-session summaries; board_search directly finds existing cards by title, notes, logs, attachment names, or current/historical columns; board_get reads one known card or the full board; board_apply changes data.',
|
|
6
|
+
'Do not call board_list before every operation. When the user names or describes an existing task but provides no ID, search global unless they explicitly restrict the request to this session.',
|
|
7
|
+
'Capability map: addColumn creates a column; addCard creates a task; renameColumn/updateCard edit title, notes, startAt/endAt, and task-time notifications; addLog appends an immutable progress note or blocker; moveCard/reorderColumns organize; archiveCard completes; restoreCard/restoreCards recover; deleteCard/deleteCards permanently delete only on explicit request.',
|
|
8
|
+
'Never guess IDs. Resolve an unknown target once with board_search or board_list, then reuse IDs and the updated board returned by successful board_apply calls. If an operation fails, refresh once, correct its arguments, and do not repeat the identical failed call.',
|
|
9
|
+
'If this session has no column, call addColumn once in session scope before adding cards. Never add a second session column.',
|
|
10
|
+
'The session UI intentionally hides the column concept even though its tasks live in that unified session-linked column.',
|
|
11
|
+
'Use updateCard to save title, notes, task times, and the timeReminder switch together; use moveCard/reorderColumns for organization.',
|
|
12
|
+
'Completing a card means archiveCard, not deleteCard. board_list reports archived counts; use board_get on the full board to inspect archivedAt, then restoreCard with a target column to recover one.',
|
|
13
|
+
'Use delete operations only when the user explicitly asks to delete data.',
|
|
14
|
+
'todo_write is this turn\'s short-lived plan. Board is for things the user wants to keep across turns.',
|
|
15
|
+
'When timeReminder is true, the board notifies once at startAt and once at endAt. Either task time may be null; when both exist startAt must be earlier than endAt.',
|
|
16
|
+
'If scope is omitted: use board_list for an overview, search global for an existing task, and create in session only when the request is tied to this conversation; otherwise ask where to create it.',
|
|
17
|
+
'Do not put card notes into this prompt; fetch them with board_get when needed.',
|
|
18
|
+
].join(' ');
|
package/lib/rpc.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import type { AttachmentDownload, AttachmentUpload, Board, BoardScope, MutateOp } from './domain.js';
|
|
3
|
+
type BoardRpcTarget = {
|
|
4
|
+
getBoard(sessionId: string, scope: BoardScope, defaultColumns?: string[]): Promise<Board>;
|
|
5
|
+
mutate(sessionId: string, scope: BoardScope, op: MutateOp, revision: number): Promise<Board>;
|
|
6
|
+
addLog(sessionId: string, scope: BoardScope, cardId: string, text: string, attachments: AttachmentUpload[], revision: number): Promise<Board>;
|
|
7
|
+
getAttachment(sessionId: string, scope: BoardScope, cardId: string, attachmentId: string): Promise<AttachmentDownload>;
|
|
8
|
+
openAttachment(sessionId: string, scope: BoardScope, cardId: string, attachmentId: string): Promise<{
|
|
9
|
+
opened: true;
|
|
10
|
+
}>;
|
|
11
|
+
};
|
|
12
|
+
export declare const BOARD_RPC_CHANNEL = "/board-rpc";
|
|
13
|
+
/** Host Connection is optional: tools still work in assemblies without a web carrier. */
|
|
14
|
+
export declare function registerBoardRpc(ctx: Context, service: BoardRpcTarget): void;
|
|
15
|
+
export declare function dispatchBoardRpc(service: BoardRpcTarget, endpoint: 'getBoard' | 'mutate' | 'addLog', payload: unknown): Promise<Board>;
|
|
16
|
+
export declare function dispatchBoardRpc(service: BoardRpcTarget, endpoint: 'getAttachment', payload: unknown): Promise<AttachmentDownload>;
|
|
17
|
+
export declare function dispatchBoardRpc(service: BoardRpcTarget, endpoint: 'openAttachment', payload: unknown): Promise<{
|
|
18
|
+
opened: true;
|
|
19
|
+
}>;
|
|
20
|
+
export declare function dispatchBoardRpc(service: BoardRpcTarget, endpoint: string, payload: unknown): Promise<unknown>;
|
|
21
|
+
export {};
|
package/lib/rpc.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { BoardNotFoundError, BoardOpError, BoardRevisionError } from './domain.js';
|
|
2
|
+
export const BOARD_RPC_CHANNEL = '/board-rpc';
|
|
3
|
+
/** Host Connection is optional: tools still work in assemblies without a web carrier. */
|
|
4
|
+
export function registerBoardRpc(ctx, service) {
|
|
5
|
+
ctx.inject(['connection'], (inner) => {
|
|
6
|
+
const connection = inner.connection;
|
|
7
|
+
connection.rpc.handle(BOARD_RPC_CHANNEL, async (endpoint, payload) => {
|
|
8
|
+
try {
|
|
9
|
+
return { ok: true, value: await dispatchBoardRpc(service, endpoint, payload) };
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
return fail(error);
|
|
13
|
+
}
|
|
14
|
+
}, { authority: 'trusted-host' });
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export async function dispatchBoardRpc(service, endpoint, payload) {
|
|
18
|
+
const args = readArgs(payload);
|
|
19
|
+
const sessionId = requireString(args, 'sessionId');
|
|
20
|
+
const scope = requireScope(args.scope);
|
|
21
|
+
if (endpoint === 'getBoard') {
|
|
22
|
+
return service.getBoard(sessionId, scope, readDefaultColumns(args.defaultColumns));
|
|
23
|
+
}
|
|
24
|
+
if (endpoint === 'mutate') {
|
|
25
|
+
const op = args.op;
|
|
26
|
+
if (op === undefined || typeof op !== 'object' || op === null || Array.isArray(op)) {
|
|
27
|
+
throw new BoardOpError('mutate requires op');
|
|
28
|
+
}
|
|
29
|
+
const revision = args.revision;
|
|
30
|
+
if (typeof revision !== 'number' || !Number.isInteger(revision)) {
|
|
31
|
+
throw new BoardOpError('mutate requires integer revision');
|
|
32
|
+
}
|
|
33
|
+
return service.mutate(sessionId, scope, op, revision);
|
|
34
|
+
}
|
|
35
|
+
if (endpoint === 'addLog') {
|
|
36
|
+
const attachments = args.attachments;
|
|
37
|
+
if (!Array.isArray(attachments))
|
|
38
|
+
throw new BoardOpError('addLog requires attachments');
|
|
39
|
+
return service.addLog(sessionId, scope, requireString(args, 'cardId'), typeof args.text === 'string' ? args.text : '', attachments, requireRevision(args.revision, 'addLog'));
|
|
40
|
+
}
|
|
41
|
+
if (endpoint === 'getAttachment') {
|
|
42
|
+
return service.getAttachment(sessionId, scope, requireString(args, 'cardId'), requireString(args, 'attachmentId'));
|
|
43
|
+
}
|
|
44
|
+
if (endpoint === 'openAttachment') {
|
|
45
|
+
return service.openAttachment(sessionId, scope, requireString(args, 'cardId'), requireString(args, 'attachmentId'));
|
|
46
|
+
}
|
|
47
|
+
throw new BoardOpError(`unknown board rpc: ${endpoint}`);
|
|
48
|
+
}
|
|
49
|
+
function requireRevision(value, endpoint) {
|
|
50
|
+
if (typeof value !== 'number' || !Number.isInteger(value))
|
|
51
|
+
throw new BoardOpError(`${endpoint} requires integer revision`);
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
function readArgs(payload) {
|
|
55
|
+
if (!isPlainObject(payload) || !isPlainObject(payload.args) || Reflect.ownKeys(payload).length !== 1) {
|
|
56
|
+
throw new BoardOpError('Remote payload must contain exactly one plain-object args field');
|
|
57
|
+
}
|
|
58
|
+
return payload.args;
|
|
59
|
+
}
|
|
60
|
+
function requireString(args, key) {
|
|
61
|
+
const value = args[key];
|
|
62
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
63
|
+
throw new BoardOpError(`args.${key} must be a non-empty string`);
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
function requireScope(value) {
|
|
68
|
+
if (value === 'global' || value === 'session')
|
|
69
|
+
return value;
|
|
70
|
+
throw new BoardOpError('args.scope must be global or session');
|
|
71
|
+
}
|
|
72
|
+
function readDefaultColumns(value) {
|
|
73
|
+
if (value === undefined)
|
|
74
|
+
return undefined;
|
|
75
|
+
if (!Array.isArray(value) || value.length !== 3 || value.some((title) => typeof title !== 'string' || title.trim().length === 0)) {
|
|
76
|
+
throw new BoardOpError('args.defaultColumns must contain three non-empty strings');
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
function fail(error) {
|
|
81
|
+
if (error instanceof BoardRevisionError) {
|
|
82
|
+
return { ok: false, error: { code: 'internal', message: error.message, details: {} } };
|
|
83
|
+
}
|
|
84
|
+
if (error instanceof BoardNotFoundError || error instanceof BoardOpError) {
|
|
85
|
+
return { ok: false, error: { code: 'internal', message: error.message, details: {} } };
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
error: {
|
|
90
|
+
code: 'internal',
|
|
91
|
+
message: error instanceof Error ? error.message : String(error),
|
|
92
|
+
details: {},
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function isPlainObject(value) {
|
|
97
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
98
|
+
return false;
|
|
99
|
+
const proto = Object.getPrototypeOf(value);
|
|
100
|
+
return proto === null || proto === Object.prototype;
|
|
101
|
+
}
|
package/lib/service.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
3
|
+
import type { AttachmentDownload, AttachmentUpload, Board, BoardScope, BoardSummary, Card, Clock, IdFactory, MutateOp } from './domain.js';
|
|
4
|
+
import { fireDueReminders } from './domain.js';
|
|
5
|
+
import type { BoardStore } from './store.js';
|
|
6
|
+
export interface BoardChangedEvent {
|
|
7
|
+
scope: BoardScope;
|
|
8
|
+
sessionId?: string;
|
|
9
|
+
board: Board;
|
|
10
|
+
}
|
|
11
|
+
export interface BoardServiceOptions {
|
|
12
|
+
now?: Clock;
|
|
13
|
+
newId?: IdFactory;
|
|
14
|
+
emit?: (event: BoardChangedEvent) => void;
|
|
15
|
+
wakeOnGlobalReminder?: boolean;
|
|
16
|
+
openFile?: (path: string) => Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export interface BoardReminderEvent extends BoardChangedEvent {
|
|
19
|
+
fired: ReturnType<typeof fireDueReminders>['fired'];
|
|
20
|
+
}
|
|
21
|
+
declare module '@deepseek-ai/cordis' {
|
|
22
|
+
interface Context {
|
|
23
|
+
board: BoardService;
|
|
24
|
+
}
|
|
25
|
+
interface Events {
|
|
26
|
+
'board/changed'(payload: BoardChangedEvent): void;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Store-backed board API. Tests construct this directly (no Cordis). */
|
|
30
|
+
export declare class BoardModel {
|
|
31
|
+
private readonly store;
|
|
32
|
+
readonly wakeOnGlobalReminder: boolean;
|
|
33
|
+
private readonly now;
|
|
34
|
+
private readonly createId;
|
|
35
|
+
private readonly emitChange;
|
|
36
|
+
private readonly openFile;
|
|
37
|
+
private mutationChain;
|
|
38
|
+
private migrationPromise;
|
|
39
|
+
private migrationChecked;
|
|
40
|
+
constructor(store: BoardStore, options?: BoardServiceOptions);
|
|
41
|
+
list(callerSessionId: string): Promise<BoardSummary[]>;
|
|
42
|
+
getBoard(callerSessionId: string, scope: BoardScope, defaultColumns?: string[]): Promise<Board>;
|
|
43
|
+
getCard(callerSessionId: string, scope: BoardScope, cardId: string): Promise<Card>;
|
|
44
|
+
mutate(callerSessionId: string, scope: BoardScope, op: MutateOp, revision?: number): Promise<Board>;
|
|
45
|
+
addLog(callerSessionId: string, scope: BoardScope, cardId: string, text: string, attachments: AttachmentUpload[], revision: number): Promise<Board>;
|
|
46
|
+
getAttachment(callerSessionId: string, scope: BoardScope, cardId: string, attachmentId: string): Promise<AttachmentDownload>;
|
|
47
|
+
openAttachment(callerSessionId: string, scope: BoardScope, cardId: string, attachmentId: string): Promise<{
|
|
48
|
+
opened: true;
|
|
49
|
+
}>;
|
|
50
|
+
fireDue(): Promise<BoardReminderEvent[]>;
|
|
51
|
+
private loadUnifiedGlobal;
|
|
52
|
+
private ensureLegacyMigration;
|
|
53
|
+
private migrateLegacySessionBoards;
|
|
54
|
+
private emitBoard;
|
|
55
|
+
private removeDeletedAttachments;
|
|
56
|
+
private enqueueMutation;
|
|
57
|
+
}
|
|
58
|
+
/** Host service: `ctx.board` plus Typert SRC Remote (`board/getBoard`, `board/mutate`). */
|
|
59
|
+
export declare class BoardService extends TypertRemoteService {
|
|
60
|
+
readonly model: BoardModel;
|
|
61
|
+
constructor(ctx: Context, store: BoardStore, options?: BoardServiceOptions);
|
|
62
|
+
get wakeOnGlobalReminder(): boolean;
|
|
63
|
+
list(callerSessionId: string): Promise<BoardSummary[]>;
|
|
64
|
+
getBoard(sessionId: string, scope: BoardScope, defaultColumns?: string[]): Promise<Board>;
|
|
65
|
+
getCard(sessionId: string, scope: BoardScope, cardId: string): Promise<Card>;
|
|
66
|
+
mutate(sessionId: string, scope: BoardScope, op: MutateOp, revision: number): Promise<Board>;
|
|
67
|
+
addLog(sessionId: string, scope: BoardScope, cardId: string, text: string, attachments: AttachmentUpload[], revision: number): Promise<Board>;
|
|
68
|
+
getAttachment(sessionId: string, scope: BoardScope, cardId: string, attachmentId: string): Promise<AttachmentDownload>;
|
|
69
|
+
openAttachment(sessionId: string, scope: BoardScope, cardId: string, attachmentId: string): Promise<{
|
|
70
|
+
opened: true;
|
|
71
|
+
}>;
|
|
72
|
+
fireDue(): Promise<BoardReminderEvent[]>;
|
|
73
|
+
}
|
|
74
|
+
export declare function createBoardService(ctx: Context, store: BoardStore, options?: BoardServiceOptions): BoardService;
|