@co0ontty/wand 4.55.3 → 4.56.0-beta.ga71d7db

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.
Files changed (55) hide show
  1. package/dist/build-info.json +4 -4
  2. package/dist/github-connector.d.ts +22 -0
  3. package/dist/github-connector.js +117 -0
  4. package/dist/missions.js +42 -9
  5. package/dist/server-github-routes.d.ts +10 -0
  6. package/dist/server-github-routes.js +239 -0
  7. package/dist/server-settings-routes.d.ts +2 -0
  8. package/dist/server-settings-routes.js +1 -0
  9. package/dist/server-task-routes.d.ts +4 -0
  10. package/dist/server-task-routes.js +99 -0
  11. package/dist/server-workspace-routes.js +70 -27
  12. package/dist/server.js +25 -0
  13. package/dist/storage.d.ts +42 -0
  14. package/dist/storage.js +223 -0
  15. package/dist/structured-provider-common.js +2 -1
  16. package/dist/task-types.d.ts +19 -0
  17. package/dist/task-types.js +1 -0
  18. package/dist/taskboard-bridge.d.ts +11 -0
  19. package/dist/taskboard-bridge.js +360 -0
  20. package/dist/web-ui/content/scripts.js +123 -127
  21. package/dist/web-ui/content/styles.css +1 -1
  22. package/dist/web-ui/embedded-assets.js +2 -2
  23. package/dist/web-ui/index.js +1 -0
  24. package/package.json +4 -2
  25. package/vendor/codex-taskboard/LICENSE +201 -0
  26. package/vendor/codex-taskboard/server/ai-chat-catalog.mjs +1031 -0
  27. package/vendor/codex-taskboard/server/ai-chat-process.mjs +462 -0
  28. package/vendor/codex-taskboard/server/ai-chat.mjs +1237 -0
  29. package/vendor/codex-taskboard/server/ai-turn-owner.mjs +39 -0
  30. package/vendor/codex-taskboard/server/app.mjs +3211 -0
  31. package/vendor/codex-taskboard/server/cloud-config.mjs +188 -0
  32. package/vendor/codex-taskboard/server/cloud-proxy.mjs +310 -0
  33. package/vendor/codex-taskboard/server/codex-app-server.mjs +376 -0
  34. package/vendor/codex-taskboard/server/codex-slash-commands-0.139.0.json +62 -0
  35. package/vendor/codex-taskboard/server/database.mjs +2845 -0
  36. package/vendor/codex-taskboard/server/index.mjs +41 -0
  37. package/vendor/codex-taskboard/server/jira-config.mjs +181 -0
  38. package/vendor/codex-taskboard/server/jira-integration.mjs +487 -0
  39. package/vendor/codex-taskboard/server/project-summary.mjs +159 -0
  40. package/vendor/codex-taskboard/shared/api-fields.mjs +163 -0
  41. package/vendor/codex-taskboard/shared/codex-environment.mjs +5 -0
  42. package/vendor/codex-taskboard/shared/codex-executable.mjs +77 -0
  43. package/vendor/codex-taskboard/shared/codex-permissions.mjs +9 -0
  44. package/vendor/codex-taskboard/shared/composer-reference.d.mts +19 -0
  45. package/vendor/codex-taskboard/shared/composer-reference.mjs +90 -0
  46. package/vendor/codex-taskboard/shared/domain.mjs +34 -0
  47. package/vendor/codex-taskboard/shared/executable-command.mjs +13 -0
  48. package/vendor/codex-taskboard/shared/process-tree.mjs +27 -0
  49. package/vendor/codex-taskboard/shared/task-input.mjs +308 -0
  50. package/vendor/codex-taskboard/shared/task-records.mjs +199 -0
  51. package/vendor/codex-taskboard/shared/task-relations.mjs +55 -0
  52. package/vendor/codex-taskboard/shared/taskboard-automation.mjs +277 -0
  53. package/vendor/codex-taskboard/skills/manage-taskboard/SKILL.md +45 -0
  54. package/vendor/codex-taskboard/skills/manage-taskboard/agents/openai.yaml +4 -0
  55. package/vendor/codex-taskboard/skills/manage-taskboard/references/cli.md +211 -0
@@ -8,7 +8,7 @@ import { checkSessionWorktreeMergeabilityAsync, cleanupWorktreeSync, prepareSess
8
8
  import { collectSessionTopicBlocklist } from "./session-topic.js";
9
9
  import { resolveSessionDisplayTitle } from "./session-transport.js";
10
10
  import { GLOBAL_WORKSPACE_ID } from "./types.js";
11
- import { attachUnboundSessionsToWorkspace, backfillSessionWorkspaces, } from "./workspace-binding.js";
11
+ import { attachUnboundSessionsToWorkspace, backfillSessionWorkspaces, normalizeProjectCwd, projectCwdForSession, } from "./workspace-binding.js";
12
12
  const PROVIDERS = new Set(["claude", "codex", "opencode", "grok", "qoder", "pi"]);
13
13
  function workspaceSessionTitle(session, names = {}) {
14
14
  return resolveSessionDisplayTitle(session, collectSessionTopicBlocklist({
@@ -71,12 +71,15 @@ function taskRuntimeCwd(task, workspace) {
71
71
  }
72
72
  function createTaskForWorkspace(storage, workspace, body) {
73
73
  const name = typeof body.name === "string" ? body.name.trim() : "";
74
- if (!name)
75
- throw new Error("请输入任务名称。");
74
+ const taskName = name || "未命名任务";
76
75
  let mountedCwd;
77
76
  if (body.cwd !== undefined && body.cwd !== null && String(body.cwd).trim()) {
78
77
  mountedCwd = resolveWorkspaceCwd(body.cwd);
79
78
  }
79
+ if (mountedCwd && !isGlobalWorkspace(workspace)
80
+ && normalizeProjectCwd(mountedCwd) !== normalizeProjectCwd(workspace.cwd)) {
81
+ throw new Error("任务目录与所属项目不一致,请先选择对应项目,或创建独立任务。");
82
+ }
80
83
  const baseRef = typeof body.baseRef === "string" && body.baseRef.trim() ? body.baseRef.trim() : undefined;
81
84
  const runCwd = mountedCwd ?? workspace.cwd;
82
85
  const wantWorktree = body.worktree === true || (body.worktree !== false && !isGlobalWorkspace(workspace));
@@ -87,7 +90,7 @@ function createTaskForWorkspace(storage, workspace, body) {
87
90
  const setup = prepareSessionWorktree({
88
91
  cwd: runCwd,
89
92
  sessionId: crypto.randomUUID(),
90
- spec: { taskName: name, baseRef },
93
+ spec: { taskName, baseRef },
91
94
  });
92
95
  worktree = setup.worktree;
93
96
  }
@@ -98,9 +101,9 @@ function createTaskForWorkspace(storage, workspace, body) {
98
101
  const storedCwd = mountedCwd && mountedCwd !== workspace.cwd ? mountedCwd : undefined;
99
102
  const task = storage.createWorkspaceTask({
100
103
  workspaceId: workspace.id,
101
- name,
102
- worktree,
104
+ name: taskName,
103
105
  cwd: storedCwd,
106
+ worktree,
104
107
  });
105
108
  return {
106
109
  task,
@@ -392,27 +395,61 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
392
395
  workspaceName: global ? "全局" : workspace.name,
393
396
  workspaceCwd: workspace.cwd,
394
397
  ...(global ? { global: true } : {}),
395
- tasks: storage.listWorkspaceTasks(workspace.id)
396
- .slice(0, taskLimit ?? undefined)
397
- .map((task) => {
398
- const allSessions = storage.listSessionsByWorkspaceTask(task.id);
399
- const sessions = allSessions
400
- .slice(0, sessionLimit ?? undefined)
401
- .map((session) => ({
402
- ...summarize(session, { taskName: task.name, workspaceName: global ? undefined : workspace.name }),
403
- workspaceTaskId: task.id,
404
- }));
405
- return {
406
- ...task,
407
- cwd: taskRuntimeCwd(task, workspace),
408
- isolated: task.worktree !== null,
409
- sessions,
410
- totalSessions: allSessions.length,
411
- };
412
- }),
398
+ tasks: [],
413
399
  standaloneSessions: [],
414
400
  });
415
401
  }
402
+ const syntheticGroupForCwd = (cwd) => {
403
+ const normalized = normalizeProjectCwd(cwd);
404
+ const existingProject = [...groups.values()].find((candidate) => !candidate.synthetic
405
+ && !candidate.global && normalizeProjectCwd(candidate.workspaceCwd) === normalized);
406
+ if (existingProject)
407
+ return existingProject;
408
+ const id = `cwd:${normalized}`;
409
+ const existing = groups.get(id);
410
+ if (existing)
411
+ return existing;
412
+ const created = {
413
+ workspaceId: id,
414
+ workspaceName: normalized.split("/").filter(Boolean).at(-1) || normalized,
415
+ workspaceCwd: normalized,
416
+ synthetic: true,
417
+ tasks: [],
418
+ standaloneSessions: [],
419
+ };
420
+ groups.set(id, created);
421
+ return created;
422
+ };
423
+ const appendTask = (target, workspace, task) => {
424
+ const allSessions = storage.listSessionsByWorkspaceTask(task.id);
425
+ const sessions = allSessions
426
+ .slice(0, sessionLimit ?? undefined)
427
+ .map((session) => ({
428
+ ...summarize(session, { taskName: task.name, workspaceName: isGlobalWorkspace(workspace) ? undefined : workspace.name }),
429
+ workspaceTaskId: task.id,
430
+ }));
431
+ target.tasks.push({
432
+ ...task,
433
+ cwd: taskRuntimeCwd(task, workspace),
434
+ isolated: task.worktree !== null,
435
+ sessions,
436
+ totalSessions: allSessions.length,
437
+ });
438
+ };
439
+ for (const workspace of visibleWorkspaces) {
440
+ const base = groups.get(workspace.id);
441
+ if (!base)
442
+ continue;
443
+ const global = isGlobalWorkspace(workspace);
444
+ for (const task of storage.listWorkspaceTasks(workspace.id).slice(0, taskLimit ?? undefined)) {
445
+ const taskCwd = taskRuntimeCwd(task, workspace);
446
+ const target = global && taskCwd
447
+ && normalizeProjectCwd(taskCwd) !== normalizeProjectCwd(workspace.cwd)
448
+ ? syntheticGroupForCwd(taskCwd)
449
+ : base;
450
+ appendTask(target, workspace, task);
451
+ }
452
+ }
416
453
  const taskBoundSessionIds = new Set();
417
454
  for (const workspace of workspaces) {
418
455
  for (const task of storage.listWorkspaceTasks(workspace.id)) {
@@ -424,10 +461,16 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
424
461
  if (taskBoundSessionIds.has(session.id))
425
462
  continue;
426
463
  const direct = session.workspaceId ? groups.get(session.workspaceId) : undefined;
427
- let group = direct && !direct.synthetic ? direct : undefined;
428
- const resolved = session.cwd ? path.resolve(session.cwd) : "";
464
+ const resolved = projectCwdForSession(session);
465
+ // 持久化的 workspaceId 可能来自旧目录或目录迁移;path 与项目目录不一致时,
466
+ // 必须按当前实际目录归类,避免不同项目的会话混入同一组。
467
+ let group = direct && !direct.synthetic
468
+ && normalizeProjectCwd(direct.workspaceCwd) === resolved
469
+ ? direct
470
+ : undefined;
429
471
  if (!group && resolved) {
430
- const cwdMatches = [...groups.values()].filter((candidate) => !candidate.synthetic && candidate.workspaceCwd === resolved);
472
+ const cwdMatches = [...groups.values()].filter((candidate) => !candidate.synthetic
473
+ && normalizeProjectCwd(candidate.workspaceCwd) === resolved);
431
474
  group = cwdMatches.find((candidate) => !candidate.global) ?? cwdMatches[0];
432
475
  }
433
476
  // 过滤模式下不创建合成组:不属于目标目录的会话直接排除。
package/dist/server.js CHANGED
@@ -22,6 +22,10 @@ import { resolveSessionAiContext, resolveSystemAiContext } from "./session-ai-co
22
22
  import { StructuredSessionManager } from "./structured-session-manager.js";
23
23
  import { recordRecentPath, registerFileRoutes } from "./server-file-routes.js";
24
24
  import { registerSettingsRoutes } from "./server-settings-routes.js";
25
+ import { registerGithubRoutes } from "./server-github-routes.js";
26
+ import { startTaskboardBridge } from "./taskboard-bridge.js";
27
+ import { registerTaskRoutes } from "./server-task-routes.js";
28
+ import { getGithubConnectorStatus } from "./github-connector.js";
25
29
  import { registerMissionRoutes } from "./server-mission-routes.js";
26
30
  import { Missions } from "./missions.js";
27
31
  import { refreshProviderCliUpdateState, registerAdminUpdateRoutes, registerPublicUpdateRoutes, ServerUpdateState, } from "./server-update-routes.js";
@@ -554,6 +558,20 @@ export async function startServer(config, configPath, options = {}) {
554
558
  const requireSessions = buildRequireScope("sessions");
555
559
  const requireFiles = buildRequireScope("files");
556
560
  const requirePasswordVault = buildRequireScope("password-vault");
561
+ const taskboardBridge = await startTaskboardBridge(configDir, storage, sessionRegistry, structuredSessions, config.defaultCwd);
562
+ app.use("/taskboard", (req, res, next) => {
563
+ // Taskboard's immutable Vite assets are safe to serve without the Wand
564
+ // session cookie. This matters behind HTTPS reverse proxies where iframe
565
+ // subresource requests may not carry the parent authentication cookie.
566
+ const staticAsset = req.path.startsWith("/assets/")
567
+ || /^\/[^/]+\.(?:css|js|map|svg|png|jpg|jpeg|webp|ico)$/.test(req.path);
568
+ if (staticAsset) {
569
+ taskboardBridge.handler(req, res, next);
570
+ return;
571
+ }
572
+ requireAuth(req, res, next);
573
+ });
574
+ app.use("/taskboard", taskboardBridge.handler);
557
575
  // Route-specific parsers must run before the global parser. Once body-parser
558
576
  // has consumed a request, a later express.json() cannot tighten or widen it.
559
577
  app.use("/api/optimize-prompt", express.json({ limit: "256kb" }));
@@ -953,6 +971,7 @@ export async function startServer(config, configPath, options = {}) {
953
971
  getCachedUpdateInfo: () => cachedUpdateInfo,
954
972
  getUpdateChannel,
955
973
  getDistributionSettings,
974
+ getGithubConnector: () => getGithubConnectorStatus(storage),
956
975
  modelCatalog,
957
976
  resolveAppConnectCode: (req) => {
958
977
  const effectivePassword = getEffectivePassword(storage, config);
@@ -964,6 +983,8 @@ export async function startServer(config, configPath, options = {}) {
964
983
  return { code: encodeConnectCode(serverUrl, token), url: serverUrl };
965
984
  },
966
985
  });
986
+ registerGithubRoutes(app, { storage, requireAdmin, sessions: sessionRegistry });
987
+ registerTaskRoutes(app, storage, sessionRegistry, requireAdmin);
967
988
  registerAdminUpdateRoutes(app, {
968
989
  storage,
969
990
  config,
@@ -1565,6 +1586,10 @@ export async function startServer(config, configPath, options = {}) {
1565
1586
  wss.close();
1566
1587
  }
1567
1588
  catch { /* ignore */ }
1589
+ try {
1590
+ await taskboardBridge.close();
1591
+ }
1592
+ catch { /* best-effort shutdown */ }
1568
1593
  try {
1569
1594
  await serverClosed;
1570
1595
  }
package/dist/storage.d.ts CHANGED
@@ -1,6 +1,13 @@
1
1
  import { SessionSnapshot, ConversationTurn, StructuredSessionState, Workspace, LayoutNode, TaskWindowLayout, WorkspaceDefaultProvider, WorkspaceKind, WorkspaceTask, WorkspaceTaskWorktree, WorkspaceTaskStatus } from "./types.js";
2
2
  import type { AgentActivityItem, Mission, MissionAttempt, MissionReviewComment, MissionReviewStatus, MissionStatus } from "./mission-types.js";
3
3
  import { type PasswordVault, type PasswordVaultItem, type PasswordVaultItemFilter, type PasswordVaultItemInput } from "./password-manager.js";
4
+ export interface ConnectorMeta {
5
+ provider: string;
6
+ apiUrl: string | null;
7
+ username: string | null;
8
+ connectedAt: string | null;
9
+ updatedAt: string;
10
+ }
4
11
  type AuthPrincipalKind = "browser-admin" | "connected-app";
5
12
  export type AuthScope = "admin" | "sessions" | "files" | "password-vault" | "session-preferences";
6
13
  export interface AuthPrincipal {
@@ -91,6 +98,31 @@ export declare class WandStorage {
91
98
  deleteWorkspaceTask(id: string, options?: {
92
99
  cascade?: boolean;
93
100
  }): void;
101
+ listWandTasks(workspaceId?: string | null): import("./task-types.js").WandTask[];
102
+ getWandTask(id: string): import("./task-types.js").WandTask | null;
103
+ createWandTask(input: {
104
+ workspaceId?: string | null;
105
+ title: string;
106
+ description?: string;
107
+ status?: import("./task-types.js").WandTaskStatus;
108
+ priority?: import("./task-types.js").WandTaskPriority;
109
+ labels?: string[];
110
+ dueDate?: string | null;
111
+ }): import("./task-types.js").WandTask;
112
+ updateWandTask(id: string, patch: Partial<Pick<import("./task-types.js").WandTask, "workspaceId" | "title" | "description" | "status" | "priority" | "labels" | "dueDate" | "sortOrder">>): import("./task-types.js").WandTask | null;
113
+ deleteWandTask(id: string): void;
114
+ listWandTaskSessionIds(taskId: string): string[];
115
+ bindWandTaskSession(taskId: string, sessionId: string): void;
116
+ unbindWandTaskSession(taskId: string, sessionId: string): void;
117
+ bindTaskboardTaskSession(taskId: string, sessionId: string): void;
118
+ listTaskboardTaskSessions(taskId: string): string[];
119
+ unbindTaskboardTaskSession(taskId: string, sessionId: string): void;
120
+ listGithubIssueBindings(owner: string, repo: string, issueNumber: number): Array<{
121
+ sessionId: string;
122
+ boundAt: string;
123
+ }>;
124
+ bindGithubIssueSession(owner: string, repo: string, issueNumber: number, sessionId: string): void;
125
+ unbindGithubIssueSession(owner: string, repo: string, issueNumber: number, sessionId: string): void;
94
126
  listSessionsByWorkspaceTask(taskId: string): SessionSnapshot[];
95
127
  setSessionWorkspaceTaskId(sessionId: string, taskId: string | null): void;
96
128
  /** Get password from database */
@@ -115,6 +147,16 @@ export declare class WandStorage {
115
147
  updatePasswordItem(id: string, input: PasswordVaultItemInput): PasswordVaultItem | null;
116
148
  touchPasswordItem(id: string): PasswordVaultItem | null;
117
149
  deletePasswordItem(id: string): boolean;
150
+ getConnectorToken(provider: string): string | null;
151
+ getConnectorMeta(provider: string): ConnectorMeta | null;
152
+ saveConnector(provider: string, input: {
153
+ token?: string | null;
154
+ apiUrl?: string | null;
155
+ username?: string | null;
156
+ markConnected?: boolean;
157
+ }): ConnectorMeta;
158
+ deleteConnector(provider: string): boolean;
159
+ private encryptConnectorToken;
118
160
  saveAuthSession(token: string, expiresAt: number, principal?: AuthPrincipal): void;
119
161
  getAuthSession(token: string): PersistedAuthSession | null;
120
162
  deleteAuthSession(token: string): void;
package/dist/storage.js CHANGED
@@ -545,6 +545,16 @@ const INIT_SQL = `
545
545
  updated_at TEXT NOT NULL
546
546
  );
547
547
 
548
+ CREATE TABLE IF NOT EXISTS connectors (
549
+ provider TEXT PRIMARY KEY,
550
+ token TEXT NOT NULL DEFAULT '',
551
+ api_url TEXT NOT NULL DEFAULT '',
552
+ username TEXT,
553
+ connected_at TEXT,
554
+ created_at TEXT NOT NULL,
555
+ updated_at TEXT NOT NULL
556
+ );
557
+
548
558
  CREATE TABLE IF NOT EXISTS password_items (
549
559
  id TEXT PRIMARY KEY,
550
560
  vault_id TEXT NOT NULL,
@@ -663,7 +673,65 @@ const INIT_SQL = `
663
673
 
664
674
  CREATE INDEX IF NOT EXISTS idx_workspace_tasks_workspace ON workspace_tasks(workspace_id);
665
675
  CREATE INDEX IF NOT EXISTS idx_workspace_tasks_last_opened ON workspace_tasks(last_opened_at);
676
+
677
+ CREATE TABLE IF NOT EXISTS github_issue_bindings (
678
+ owner TEXT NOT NULL,
679
+ repo TEXT NOT NULL,
680
+ issue_number INTEGER NOT NULL,
681
+ session_id TEXT NOT NULL,
682
+ bound_at TEXT NOT NULL,
683
+ PRIMARY KEY (owner, repo, issue_number, session_id),
684
+ FOREIGN KEY (session_id) REFERENCES command_sessions(id) ON DELETE CASCADE
685
+ );
686
+ CREATE INDEX IF NOT EXISTS idx_github_issue_bindings_session ON github_issue_bindings(session_id);
687
+
688
+ CREATE TABLE IF NOT EXISTS wand_tasks (
689
+ id TEXT PRIMARY KEY,
690
+ identifier TEXT,
691
+ workspace_id TEXT,
692
+ title TEXT NOT NULL,
693
+ description TEXT NOT NULL DEFAULT '',
694
+ status TEXT NOT NULL DEFAULT 'todo',
695
+ priority TEXT NOT NULL DEFAULT 'none',
696
+ labels_json TEXT NOT NULL DEFAULT '[]',
697
+ due_date TEXT,
698
+ sort_order INTEGER NOT NULL DEFAULT 0,
699
+ created_at TEXT NOT NULL,
700
+ updated_at TEXT NOT NULL,
701
+ FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE SET NULL
702
+ );
703
+ CREATE INDEX IF NOT EXISTS idx_wand_tasks_board ON wand_tasks(workspace_id, status, sort_order, updated_at);
704
+ CREATE TABLE IF NOT EXISTS wand_task_sessions (
705
+ task_id TEXT NOT NULL,
706
+ session_id TEXT NOT NULL,
707
+ bound_at TEXT NOT NULL,
708
+ PRIMARY KEY (task_id, session_id),
709
+ FOREIGN KEY (task_id) REFERENCES wand_tasks(id) ON DELETE CASCADE,
710
+ FOREIGN KEY (session_id) REFERENCES command_sessions(id) ON DELETE CASCADE
711
+ );
712
+ CREATE INDEX IF NOT EXISTS idx_wand_task_sessions_session ON wand_task_sessions(session_id);
713
+ CREATE TABLE IF NOT EXISTS taskboard_session_bindings (
714
+ task_id TEXT NOT NULL,
715
+ session_id TEXT NOT NULL,
716
+ bound_at TEXT NOT NULL,
717
+ PRIMARY KEY (task_id, session_id),
718
+ FOREIGN KEY (session_id) REFERENCES command_sessions(id) ON DELETE CASCADE
719
+ );
720
+ CREATE INDEX IF NOT EXISTS idx_taskboard_session_bindings_session ON taskboard_session_bindings(session_id);
666
721
  `;
722
+ function ensureWandTaskSchema(db) {
723
+ const columns = db.prepare("PRAGMA table_info(wand_tasks)").all();
724
+ const names = new Set(columns.map((column) => column.name));
725
+ if (columns.length > 0 && !names.has("identifier"))
726
+ db.exec("ALTER TABLE wand_tasks ADD COLUMN identifier TEXT");
727
+ if (columns.length > 0 && !names.has("due_date"))
728
+ db.exec("ALTER TABLE wand_tasks ADD COLUMN due_date TEXT");
729
+ if (columns.length > 0) {
730
+ const rows = db.prepare("SELECT id FROM wand_tasks WHERE identifier IS NULL OR identifier = '' ORDER BY created_at, id").all();
731
+ const update = db.prepare("UPDATE wand_tasks SET identifier = ? WHERE id = ?");
732
+ rows.forEach((row, index) => update.run(`TASK-${index + 1}`, row.id));
733
+ }
734
+ }
667
735
  function ensureWorkspaceSchema(db) {
668
736
  const workspaceColumns = db.prepare("PRAGMA table_info(workspaces)").all();
669
737
  const workspaceNames = new Set(workspaceColumns.map((column) => column.name));
@@ -676,6 +744,23 @@ function ensureWorkspaceSchema(db) {
676
744
  db.exec("ALTER TABLE workspace_tasks ADD COLUMN cwd TEXT");
677
745
  }
678
746
  }
747
+ function ensureConnectorSchema(db) {
748
+ // The table is created by INIT_SQL for new databases; this repairs an
749
+ // existing wand.db that predates the connector feature (additive-only).
750
+ const columns = db.prepare("PRAGMA table_info(connectors)").all();
751
+ const names = new Set(columns.map((column) => column.name));
752
+ if (names.size === 0)
753
+ return;
754
+ const migrations = [
755
+ ["api_url", "ALTER TABLE connectors ADD COLUMN api_url TEXT NOT NULL DEFAULT ''"],
756
+ ["username", "ALTER TABLE connectors ADD COLUMN username TEXT"],
757
+ ["connected_at", "ALTER TABLE connectors ADD COLUMN connected_at TEXT"],
758
+ ];
759
+ for (const [column, sql] of migrations) {
760
+ if (!names.has(column))
761
+ db.exec(sql);
762
+ }
763
+ }
679
764
  export function ensureDatabaseFile(dbPath) {
680
765
  const dir = path.dirname(dbPath);
681
766
  mkdirSync(dir, { recursive: true, mode: 0o700 });
@@ -686,6 +771,7 @@ export function ensureDatabaseFile(dbPath) {
686
771
  ensureAuthSessionSchema(db);
687
772
  ensureCommandSessionSchema(db);
688
773
  ensureWorkspaceSchema(db);
774
+ ensureConnectorSchema(db);
689
775
  {
690
776
  const missionColumns = db.prepare("PRAGMA table_info(missions)").all();
691
777
  if (missionColumns.length > 0 && !missionColumns.some((column) => column.name === "task_id")) {
@@ -710,6 +796,8 @@ export class WandStorage {
710
796
  ensureAuthSessionSchema(this.db);
711
797
  ensureCommandSessionSchema(this.db);
712
798
  ensureWorkspaceSchema(this.db);
799
+ ensureWandTaskSchema(this.db);
800
+ ensureConnectorSchema(this.db);
713
801
  this.ensureDefaultPasswordVault();
714
802
  }
715
803
  directory() {
@@ -1008,6 +1096,86 @@ export class WandStorage {
1008
1096
  }
1009
1097
  this.db.prepare("DELETE FROM workspace_tasks WHERE id = ?").run(id);
1010
1098
  }
1099
+ listWandTasks(workspaceId) {
1100
+ const rows = this.db.prepare(`SELECT id, identifier, workspace_id, title, description, status, priority, labels_json, due_date, sort_order, created_at, updated_at
1101
+ FROM wand_tasks ${workspaceId === undefined ? "" : "WHERE workspace_id IS ?"}
1102
+ ORDER BY status, sort_order, updated_at DESC`).all(...(workspaceId === undefined ? [] : [workspaceId]));
1103
+ return rows.map((row) => ({
1104
+ id: String(row.id),
1105
+ identifier: typeof row.identifier === "string" && row.identifier ? row.identifier : `TASK-${String(row.id).slice(0, 6)}`,
1106
+ workspaceId: typeof row.workspace_id === "string" ? row.workspace_id : null,
1107
+ title: String(row.title),
1108
+ description: String(row.description ?? ""),
1109
+ status: row.status === "doing" || row.status === "done" ? row.status : "todo",
1110
+ priority: row.priority === "low" || row.priority === "medium" || row.priority === "high" || row.priority === "urgent" ? row.priority : "none",
1111
+ labels: (() => { const parsed = safeJsonParse(String(row.labels_json ?? "[]")); return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : []; })(),
1112
+ dueDate: typeof row.due_date === "string" ? row.due_date : null,
1113
+ sortOrder: Number(row.sort_order) || 0,
1114
+ createdAt: String(row.created_at),
1115
+ updatedAt: String(row.updated_at),
1116
+ }));
1117
+ }
1118
+ getWandTask(id) {
1119
+ return this.listWandTasks().find((task) => task.id === id) ?? null;
1120
+ }
1121
+ createWandTask(input) {
1122
+ const id = crypto.randomUUID();
1123
+ const now = nowIso();
1124
+ const status = input.status ?? "todo";
1125
+ const priority = input.priority ?? "none";
1126
+ const max = this.db.prepare("SELECT COALESCE(MAX(sort_order), -1) AS value FROM wand_tasks WHERE workspace_id IS ? AND status = ?").get(input.workspaceId ?? null, status);
1127
+ const numberRow = this.db.prepare("SELECT COALESCE(MAX(CAST(substr(identifier, 6) AS INTEGER)), 0) AS value FROM wand_tasks WHERE identifier GLOB 'TASK-[0-9]*'").get();
1128
+ const identifier = `TASK-${(numberRow?.value ?? 0) + 1}`;
1129
+ this.db.prepare(`INSERT INTO wand_tasks (id, identifier, workspace_id, title, description, status, priority, labels_json, due_date, sort_order, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, identifier, input.workspaceId ?? null, input.title, input.description ?? "", status, priority, JSON.stringify(input.labels ?? []), input.dueDate ?? null, (max?.value ?? -1) + 1, now, now);
1130
+ return this.getWandTask(id);
1131
+ }
1132
+ updateWandTask(id, patch) {
1133
+ const current = this.getWandTask(id);
1134
+ if (!current)
1135
+ return null;
1136
+ const next = { ...current, ...patch, updatedAt: nowIso() };
1137
+ this.db.prepare(`UPDATE wand_tasks SET workspace_id = ?, title = ?, description = ?, status = ?, priority = ?, labels_json = ?, due_date = ?, sort_order = ?, updated_at = ? WHERE id = ?`).run(next.workspaceId, next.title, next.description, next.status, next.priority, JSON.stringify(next.labels), next.dueDate, next.sortOrder, next.updatedAt, id);
1138
+ return next;
1139
+ }
1140
+ deleteWandTask(id) { this.db.prepare("DELETE FROM wand_tasks WHERE id = ?").run(id); }
1141
+ listWandTaskSessionIds(taskId) {
1142
+ const rows = this.db.prepare("SELECT session_id FROM wand_task_sessions WHERE task_id = ? ORDER BY bound_at ASC").all(taskId);
1143
+ return rows.map((row) => row.session_id);
1144
+ }
1145
+ bindWandTaskSession(taskId, sessionId) {
1146
+ if (!this.getWandTask(taskId))
1147
+ throw new Error("未找到该任务。");
1148
+ if (!this.getSession(sessionId))
1149
+ throw new Error("未找到该会话。");
1150
+ this.db.prepare("INSERT OR IGNORE INTO wand_task_sessions (task_id, session_id, bound_at) VALUES (?, ?, ?)").run(taskId, sessionId, nowIso());
1151
+ }
1152
+ unbindWandTaskSession(taskId, sessionId) { this.db.prepare("DELETE FROM wand_task_sessions WHERE task_id = ? AND session_id = ?").run(taskId, sessionId); }
1153
+ bindTaskboardTaskSession(taskId, sessionId) {
1154
+ if (!this.getSession(sessionId))
1155
+ throw new Error("未找到该会话。");
1156
+ this.db.prepare("INSERT OR IGNORE INTO taskboard_session_bindings (task_id, session_id, bound_at) VALUES (?, ?, ?)").run(taskId, sessionId, nowIso());
1157
+ }
1158
+ listTaskboardTaskSessions(taskId) {
1159
+ const rows = this.db.prepare("SELECT session_id FROM taskboard_session_bindings WHERE task_id = ? ORDER BY rowid ASC").all(taskId);
1160
+ return rows.map((row) => row.session_id);
1161
+ }
1162
+ unbindTaskboardTaskSession(taskId, sessionId) {
1163
+ this.db.prepare("DELETE FROM taskboard_session_bindings WHERE task_id = ? AND session_id = ?").run(taskId, sessionId);
1164
+ }
1165
+ listGithubIssueBindings(owner, repo, issueNumber) {
1166
+ const rows = this.db.prepare(`SELECT session_id, bound_at FROM github_issue_bindings
1167
+ WHERE owner = ? AND repo = ? AND issue_number = ? ORDER BY bound_at ASC`).all(owner, repo, issueNumber);
1168
+ return rows.map((row) => ({ sessionId: row.session_id, boundAt: row.bound_at }));
1169
+ }
1170
+ bindGithubIssueSession(owner, repo, issueNumber, sessionId) {
1171
+ if (!this.getSession(sessionId))
1172
+ throw new Error("未找到该会话。");
1173
+ this.db.prepare(`INSERT OR IGNORE INTO github_issue_bindings (owner, repo, issue_number, session_id, bound_at)
1174
+ VALUES (?, ?, ?, ?, ?)`).run(owner, repo, issueNumber, sessionId, nowIso());
1175
+ }
1176
+ unbindGithubIssueSession(owner, repo, issueNumber, sessionId) {
1177
+ this.db.prepare(`DELETE FROM github_issue_bindings WHERE owner = ? AND repo = ? AND issue_number = ? AND session_id = ?`).run(owner, repo, issueNumber, sessionId);
1178
+ }
1011
1179
  listSessionsByWorkspaceTask(taskId) {
1012
1180
  const rows = this.db
1013
1181
  .prepare(`${sessionRowQuery("SELECT")}
@@ -1164,6 +1332,61 @@ export class WandStorage {
1164
1332
  .run(now, id);
1165
1333
  return result.changes > 0;
1166
1334
  }
1335
+ // ============ Connector Methods ============
1336
+ // 第三方服务凭据(GitHub 等)。token 用与密码库同一把 at-rest 密钥加密,
1337
+ // 解密只在服务端发生,绝不通过 /api/settings 回传明文。
1338
+ getConnectorToken(provider) {
1339
+ const row = this.db
1340
+ .prepare("SELECT token FROM connectors WHERE provider = ?")
1341
+ .get(provider);
1342
+ if (!row)
1343
+ return null;
1344
+ const decrypted = decryptVaultSecret(row.token, this.getAppSecret());
1345
+ return decrypted ?? "";
1346
+ }
1347
+ getConnectorMeta(provider) {
1348
+ const row = this.db
1349
+ .prepare("SELECT provider, api_url, username, connected_at, updated_at FROM connectors WHERE provider = ?")
1350
+ .get(provider);
1351
+ if (!row)
1352
+ return null;
1353
+ return {
1354
+ provider: row.provider,
1355
+ apiUrl: row.api_url || null,
1356
+ username: row.username ?? null,
1357
+ connectedAt: row.connected_at ?? null,
1358
+ updatedAt: row.updated_at,
1359
+ };
1360
+ }
1361
+ saveConnector(provider, input) {
1362
+ const now = nowIso();
1363
+ const existing = this.getConnectorMeta(provider);
1364
+ const token = input.token === undefined || input.token === null || input.token === ""
1365
+ ? this.getConnectorToken(provider) ?? ""
1366
+ : input.token;
1367
+ const apiUrl = input.apiUrl === undefined ? (existing?.apiUrl ?? "") : input.apiUrl;
1368
+ const username = input.username === undefined ? (existing?.username ?? null) : input.username;
1369
+ const connectedAt = input.markConnected === false ? (existing?.connectedAt ?? now) : now;
1370
+ const encrypted = this.encryptConnectorToken(token);
1371
+ this.db.prepare(`INSERT INTO connectors (provider, token, api_url, username, connected_at, created_at, updated_at)
1372
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1373
+ ON CONFLICT(provider) DO UPDATE SET
1374
+ token = excluded.token,
1375
+ api_url = excluded.api_url,
1376
+ username = excluded.username,
1377
+ connected_at = excluded.connected_at,
1378
+ updated_at = excluded.updated_at`).run(provider, encrypted, apiUrl, username, connectedAt, now, now);
1379
+ return { provider, apiUrl: apiUrl || null, username, connectedAt, updatedAt: now };
1380
+ }
1381
+ deleteConnector(provider) {
1382
+ return this.db.prepare("DELETE FROM connectors WHERE provider = ?").run(provider).changes > 0;
1383
+ }
1384
+ encryptConnectorToken(token) {
1385
+ if (!token)
1386
+ return "";
1387
+ const secret = this.getAppSecret();
1388
+ return secret ? encryptVaultSecret(token, secret) : token;
1389
+ }
1167
1390
  // ============ Auth Session Methods ============
1168
1391
  saveAuthSession(token, expiresAt, principal = { kind: "browser-admin", scopes: ["admin"] }) {
1169
1392
  this.db
@@ -116,7 +116,8 @@ export function thinkingEffortToPiLevel(effort) {
116
116
  return "low";
117
117
  if (effort === "deep")
118
118
  return "high";
119
+ // Pi uses `max`; `xhigh` is a Codex-only level and makes Pi reject the run.
119
120
  if (effort === "max")
120
- return "xhigh";
121
+ return "max";
121
122
  return effort.startsWith("codex:") ? effort.slice("codex:".length) || null : null;
122
123
  }
@@ -0,0 +1,19 @@
1
+ export type WandTaskStatus = "todo" | "doing" | "done";
2
+ export type WandTaskPriority = "none" | "low" | "medium" | "high" | "urgent";
3
+ export interface WandTask {
4
+ id: string;
5
+ workspaceId: string | null;
6
+ identifier: string;
7
+ title: string;
8
+ description: string;
9
+ status: WandTaskStatus;
10
+ priority: WandTaskPriority;
11
+ labels: string[];
12
+ dueDate: string | null;
13
+ sortOrder: number;
14
+ createdAt: string;
15
+ updatedAt: string;
16
+ }
17
+ export interface WandTaskDetail extends WandTask {
18
+ sessionIds: string[];
19
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ import type { RequestHandler } from "express";
2
+ import type { WandStorage } from "./storage.js";
3
+ import type { SessionRegistry } from "./session-registry.js";
4
+ import type { StructuredSessionManager } from "./structured-session-manager.js";
5
+ interface TaskboardBridge {
6
+ handler: RequestHandler;
7
+ close(): Promise<void>;
8
+ }
9
+ export declare function taskboardIntegrationScript(): string;
10
+ export declare function startTaskboardBridge(configDir: string, storage?: WandStorage, sessions?: SessionRegistry, structured?: StructuredSessionManager, fallbackCwd?: string): Promise<TaskboardBridge>;
11
+ export {};