circuschief 0.8.0 → 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.
Files changed (136) hide show
  1. package/package.json +1 -1
  2. package/packages/server/src/api/commandButtons.js +16 -15
  3. package/packages/server/src/api/projects-commandButtons.js +6 -6
  4. package/packages/server/src/api/projects-session-create.js +109 -0
  5. package/packages/server/src/api/projects-session-defaults.js +51 -0
  6. package/packages/server/src/api/projects-session-helpers.js +47 -1
  7. package/packages/server/src/api/projects-templates.js +38 -0
  8. package/packages/server/src/api/projects.js +28 -180
  9. package/packages/server/src/api/sessions-commands.js +21 -18
  10. package/packages/server/src/api/sessions-patch.js +41 -1
  11. package/packages/server/src/db/SessionRepository.js +1 -1
  12. package/packages/server/src/db/SessionTemplateRepository.js +23 -2
  13. package/packages/server/src/db/migrations/canvasItemsMigrations.js +109 -0
  14. package/packages/server/src/db/migrations/conversationsMigrations.js +187 -0
  15. package/packages/server/src/db/migrations/index.js +225 -6
  16. package/packages/server/src/db/migrations/kanbanMigrations.js +99 -0
  17. package/packages/server/src/db/migrations/miscMigrations.js +244 -0
  18. package/packages/server/src/db/migrations/projectsMigrations.js +130 -0
  19. package/packages/server/src/db/migrations/providerCommitAttributionMigrations.js +30 -0
  20. package/packages/server/src/db/migrations/providerMigrations.js +165 -0
  21. package/packages/server/src/db/migrations/sessionTableRecreate.js +136 -0
  22. package/packages/server/src/db/migrations/sessionsMigrations.js +300 -0
  23. package/packages/server/src/db/session-helpers.js +26 -1
  24. package/packages/server/src/schema.sql +4 -0
  25. package/packages/server/src/services/commandButtonPrompts.js +9 -7
  26. package/packages/server/src/services/gitCommitAttribution.js +38 -8
  27. package/packages/server/src/services/gitDiff.js +132 -0
  28. package/packages/server/src/services/gitRepoUrl.js +174 -0
  29. package/packages/server/src/services/gitService.js +37 -309
  30. package/packages/server/src/services/gitWorktree.js +127 -0
  31. package/packages/server/src/services/sessionPrompts.js +1 -1
  32. package/packages/shared/src/contracts/sessions.js +27 -1
  33. package/packages/shared/src/contracts/templates.js +10 -0
  34. package/packages/web/dist/assets/{ActiveSessionsView-B0XHqLmv.js → ActiveSessionsView-Cxh8mHmB.js} +1 -1
  35. package/packages/web/dist/assets/{AgentLogsView-DmsjUMlB.js → AgentLogsView-xdfI2bR6.js} +2 -2
  36. package/packages/web/dist/assets/ApiClient-DfbJwzpz.js +1 -0
  37. package/packages/web/dist/assets/ArchiveConfirmModal-DXZYdzHR.js +1 -0
  38. package/packages/web/dist/assets/CommandButtonDetailView-D8xfqLAp.js +1 -0
  39. package/packages/web/dist/assets/CommandButtonDetailView-D9zjx9ME.css +1 -0
  40. package/packages/web/dist/assets/EffortLevelSelector-D2Hdzc_8.js +1 -0
  41. package/packages/web/dist/assets/{GeneralSettingsView-D1nI8_zk.js → GeneralSettingsView-sPXkLlLy.js} +1 -1
  42. package/packages/web/dist/assets/{InputWithButton-CAkttyqx.js → InputWithButton-B-o0DgMH.js} +1 -1
  43. package/packages/web/dist/assets/{InterpolationHelp-BO1j9Z3_.js → InterpolationHelp-Dxn1li4l.js} +1 -1
  44. package/packages/web/dist/assets/MarkdownEditor-D4Kbb-9l.js +2 -0
  45. package/packages/web/dist/assets/ModelSelector-72C7MUH4.js +1 -0
  46. package/packages/web/dist/assets/{ModelSelector-BSxKUSus.css → ModelSelector-BNYKujL-.css} +1 -1
  47. package/packages/web/dist/assets/NewSessionView-BR_COfgW.js +3 -0
  48. package/packages/web/dist/assets/{NewSessionView-BDPb-1qr.css → NewSessionView-DBl7T2Xp.css} +1 -1
  49. package/packages/web/dist/assets/ProjectEditView-DbqTbA0q.css +1 -0
  50. package/packages/web/dist/assets/ProjectEditView-WImU7sNd.js +1 -0
  51. package/packages/web/dist/assets/{ProjectListView-DcNyuINs.js → ProjectListView-CYmmAcBD.js} +1 -1
  52. package/packages/web/dist/assets/{ProjectNewView-B5YV62hv.js → ProjectNewView-DEhqw3Jv.js} +1 -1
  53. package/packages/web/dist/assets/ProvidersView-XZh3jkmH.js +1 -0
  54. package/packages/web/dist/assets/QuickResponsesPanel-BqmnTd-D.js +1 -0
  55. package/packages/web/dist/assets/QuickResponsesPanel-dk-Rj8xx.css +1 -0
  56. package/packages/web/dist/assets/ResizableTextarea-BQNw5e0C.css +1 -0
  57. package/packages/web/dist/assets/ResizableTextarea-DpWdIAP6.js +1 -0
  58. package/packages/web/dist/assets/SessionCard-Bw77-KwD.js +1 -0
  59. package/packages/web/dist/assets/SessionDetailView-B59TEkr-.js +36 -0
  60. package/packages/web/dist/assets/SessionDetailView-CKVBnR4T.css +1 -0
  61. package/packages/web/dist/assets/{SessionFormOptions-B6AxyREh.js → SessionFormOptions-hqijxc0S.js} +1 -1
  62. package/packages/web/dist/assets/{SessionListView-B5_6gW49.css → SessionListView-3-xx6EVs.css} +1 -1
  63. package/packages/web/dist/assets/SessionListView-DYXHM9I-.js +1 -0
  64. package/packages/web/dist/assets/{SessionLogStream-LlZ3z_Xj.js → SessionLogStream-5NfVr9pF.js} +6 -6
  65. package/packages/web/dist/assets/{SettingsView-CTGiGvR2.js → SettingsView-DI8ncOAV.js} +1 -1
  66. package/packages/web/dist/assets/{SlashCommandWizard-Cy04d7-o.js → SlashCommandWizard-BQ_rMzn-.js} +1 -1
  67. package/packages/web/dist/assets/{SummarySettingsView-BR2ZjEa3.js → SummarySettingsView-C2Qs35mm.js} +1 -1
  68. package/packages/web/dist/assets/TemplateDetailView-B5NI2oTR.css +1 -0
  69. package/packages/web/dist/assets/TemplateDetailView-zVkIvgtu.js +1 -0
  70. package/packages/web/dist/assets/{commandButtons-BfqR-fqq.js → commandButtons-CoU3G4zK.js} +1 -1
  71. package/packages/web/dist/assets/index-9yF1uCCA.js +1 -0
  72. package/packages/web/dist/assets/index-BKstCaYU.js +1 -0
  73. package/packages/web/dist/assets/index-BhbH7eOk.js +1 -0
  74. package/packages/web/dist/assets/{index-DgkC10TW.js → index-BjuRttEY.js} +3 -3
  75. package/packages/web/dist/assets/index-Bo7PdwM5.js +1 -0
  76. package/packages/web/dist/assets/index-C2QFVD7d.js +83 -0
  77. package/packages/web/dist/assets/index-C7Ww2auW.js +1 -0
  78. package/packages/web/dist/assets/index-CAGdsDh7.js +1 -0
  79. package/packages/web/dist/assets/index-CLRsVASf.js +3 -0
  80. package/packages/web/dist/assets/{index-DtfUt785.js → index-CP-SxOlV.js} +1 -1
  81. package/packages/web/dist/assets/index-CslU0psO.js +1 -0
  82. package/packages/web/dist/assets/index-DI4NxaWD.js +1 -0
  83. package/packages/web/dist/assets/index-DOzONENy.js +1 -0
  84. package/packages/web/dist/assets/index-DUa7adFh.js +1 -0
  85. package/packages/web/dist/assets/index-DZBpETI5.js +1 -0
  86. package/packages/web/dist/assets/index-DsjWqc6R.js +7 -0
  87. package/packages/web/dist/assets/index-c99Bo3JV.js +1 -0
  88. package/packages/web/dist/assets/index-mT1JpxDc.js +1 -0
  89. package/packages/web/dist/assets/index-rkQx2tso.js +1 -0
  90. package/packages/web/dist/assets/{index-BY174HVJ.css → index-uySCcnA_.css} +1 -1
  91. package/packages/web/dist/assets/projectDefaults-B8esIcYq.js +1 -0
  92. package/packages/web/dist/assets/{projects-DXYQNJIi.js → projects-C-8PSxKi.js} +1 -1
  93. package/packages/web/dist/assets/{providers-1bnH-exJ.js → providers-oXifvvqN.js} +1 -1
  94. package/packages/web/dist/assets/{sessions-6zGUlFrt.js → sessions-Nq5VafSf.js} +1 -1
  95. package/packages/web/dist/assets/{settings-MbfRir0d.js → settings-DtpuiyT6.js} +1 -1
  96. package/packages/web/dist/index.html +2 -2
  97. package/packages/web/dist/assets/ApiClient-C3ztI9s9.js +0 -1
  98. package/packages/web/dist/assets/ArchiveConfirmModal-BlCyn5Vt.js +0 -1
  99. package/packages/web/dist/assets/CommandButtonDetailView-CdSCPp78.js +0 -1
  100. package/packages/web/dist/assets/CommandButtonDetailView-DBm3rzhw.css +0 -1
  101. package/packages/web/dist/assets/EffortLevelSelector-hc2MNKg6.js +0 -1
  102. package/packages/web/dist/assets/MarkdownEditor-ucRAP_UM.js +0 -2
  103. package/packages/web/dist/assets/ModelSelector-CwTz8ZWO.js +0 -1
  104. package/packages/web/dist/assets/NewSessionView-BsDrp8mj.js +0 -3
  105. package/packages/web/dist/assets/ProjectEditView-CwTOeSun.js +0 -1
  106. package/packages/web/dist/assets/ProjectEditView-J15mcsWz.css +0 -1
  107. package/packages/web/dist/assets/ProvidersView-nY9GnDdO.js +0 -1
  108. package/packages/web/dist/assets/QuickResponseSettings-B352c75l.css +0 -1
  109. package/packages/web/dist/assets/QuickResponseSettings-BQwQXuL7.js +0 -1
  110. package/packages/web/dist/assets/QuickResponsesPanel-BlFDvnZ2.css +0 -1
  111. package/packages/web/dist/assets/QuickResponsesPanel-BzSYcCSP.js +0 -1
  112. package/packages/web/dist/assets/ResizableTextarea-B3YIdIXv.js +0 -1
  113. package/packages/web/dist/assets/ResizableTextarea-DsU3TVwF.css +0 -1
  114. package/packages/web/dist/assets/SessionCard-CjE1tXiT.js +0 -1
  115. package/packages/web/dist/assets/SessionDetailView-3cPZrbS3.js +0 -36
  116. package/packages/web/dist/assets/SessionDetailView-CZRZMrfM.css +0 -1
  117. package/packages/web/dist/assets/SessionListView-CLXBfLcq.js +0 -1
  118. package/packages/web/dist/assets/TemplateDetailView-DH6Oswsp.js +0 -1
  119. package/packages/web/dist/assets/TemplateDetailView-DT2m06W7.css +0 -1
  120. package/packages/web/dist/assets/index-1zziPL6l.js +0 -1
  121. package/packages/web/dist/assets/index-7kzHPxSF.js +0 -1
  122. package/packages/web/dist/assets/index-B0N_obMc.js +0 -1
  123. package/packages/web/dist/assets/index-BNk_gdfI.js +0 -1
  124. package/packages/web/dist/assets/index-CSqaAH-0.js +0 -1
  125. package/packages/web/dist/assets/index-C_q4WlK8.js +0 -1
  126. package/packages/web/dist/assets/index-D1wpU4y0.js +0 -7
  127. package/packages/web/dist/assets/index-D5zCA8sD.js +0 -1
  128. package/packages/web/dist/assets/index-DGR8ELWY.js +0 -1
  129. package/packages/web/dist/assets/index-DHga8pXo.js +0 -1
  130. package/packages/web/dist/assets/index-DSby02Wl.js +0 -1
  131. package/packages/web/dist/assets/index-DqjXJTVI.js +0 -1
  132. package/packages/web/dist/assets/index-_4S2uLDI.js +0 -1
  133. package/packages/web/dist/assets/index-fK8FIZgP.js +0 -83
  134. package/packages/web/dist/assets/index-gmiZeFXN.js +0 -1
  135. package/packages/web/dist/assets/index-irD539ZM.js +0 -3
  136. package/packages/web/dist/assets/index-yq-E1Y00.js +0 -1
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Helper for recreating the sessions table during migrations that change
3
+ * column defaults or constraints (SQLite requires table recreation for these).
4
+ */
5
+ import { getColumns } from './migrationUtils.js';
6
+
7
+ const TABLE_SESSIONS = 'sessions';
8
+
9
+ const SESSIONS_TARGET_MODE_DEFAULT = "'yolo'";
10
+ const SESSIONS_TARGET_THINKING_ENABLED_DEFAULT = '1';
11
+
12
+ /**
13
+ * SQL column definitions for the sessions table with current defaults.
14
+ */
15
+ export const SESSIONS_ALL_CURRENT_COLUMNS = `
16
+ id TEXT PRIMARY KEY,
17
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
18
+ name TEXT NOT NULL,
19
+ status TEXT NOT NULL DEFAULT 'starting' CHECK (status IN ('starting', 'running', 'waiting', 'stopped', 'completed', 'error', 'scheduled')),
20
+ mode TEXT NOT NULL DEFAULT 'yolo' CHECK (mode IN ('plan', 'standard', 'yolo')),
21
+ thinking_enabled INTEGER NOT NULL DEFAULT 1,
22
+ archived INTEGER NOT NULL DEFAULT 0,
23
+ git_branch TEXT,
24
+ git_worktree TEXT,
25
+ pr_url TEXT,
26
+ error TEXT,
27
+ effort_level TEXT CHECK(effort_level IN ('low', 'medium', 'high', 'max', 'auto')),
28
+ cost_usd REAL DEFAULT 0,
29
+ claude_session_id TEXT,
30
+ model TEXT,
31
+ provider_id TEXT,
32
+ next_template_id TEXT REFERENCES session_templates(id) ON DELETE SET NULL,
33
+ parent_session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL,
34
+ input_tokens INTEGER DEFAULT 0,
35
+ output_tokens INTEGER DEFAULT 0,
36
+ thinking_tokens INTEGER DEFAULT 0,
37
+ cache_read_input_tokens INTEGER DEFAULT 0,
38
+ cache_creation_input_tokens INTEGER DEFAULT 0,
39
+ web_search_requests INTEGER DEFAULT 0,
40
+ context_window INTEGER DEFAULT 200000,
41
+ starred INTEGER NOT NULL DEFAULT 0,
42
+ manually_named INTEGER NOT NULL DEFAULT 0,
43
+ scheduled_at INTEGER DEFAULT NULL,
44
+ reschedule_delay_minutes INTEGER DEFAULT 15,
45
+ auto_reschedule_enabled INTEGER DEFAULT 0,
46
+ reschedule_on_token_limit INTEGER DEFAULT 1,
47
+ reschedule_on_service_error INTEGER DEFAULT 1,
48
+ max_reschedule_count INTEGER DEFAULT NULL,
49
+ max_total_tokens INTEGER DEFAULT NULL,
50
+ reschedule_count INTEGER DEFAULT 0,
51
+ reschedule_at_token_count INTEGER DEFAULT NULL,
52
+ pending_prompt TEXT,
53
+ slash_commands TEXT,
54
+ pending_model TEXT,
55
+ auto_send_pending_prompt INTEGER DEFAULT 0,
56
+ agent_type TEXT DEFAULT 'claude-code',
57
+ target_lane_id TEXT REFERENCES kanban_lanes(id) ON DELETE SET NULL,
58
+ lane_trigger_depth INTEGER NOT NULL DEFAULT 0,
59
+ created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
60
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
61
+ `;
62
+
63
+ export const SESSIONS_ALL_CURRENT_COLUMN_NAMES = [
64
+ 'id', 'project_id', 'name', 'status', 'mode', 'thinking_enabled',
65
+ 'archived', 'git_branch', 'git_worktree', 'pr_url', 'error',
66
+ 'effort_level', 'cost_usd', 'claude_session_id', 'model', 'provider_id',
67
+ 'next_template_id', 'parent_session_id', 'input_tokens', 'output_tokens',
68
+ 'thinking_tokens', 'cache_read_input_tokens', 'cache_creation_input_tokens',
69
+ 'web_search_requests', 'context_window', 'starred', 'manually_named',
70
+ 'scheduled_at', 'reschedule_delay_minutes', 'auto_reschedule_enabled',
71
+ 'reschedule_on_token_limit', 'reschedule_on_service_error',
72
+ 'max_reschedule_count', 'max_total_tokens', 'reschedule_count',
73
+ 'reschedule_at_token_count', 'pending_prompt', 'slash_commands',
74
+ 'pending_model', 'auto_send_pending_prompt', 'agent_type', 'target_lane_id',
75
+ 'lane_trigger_depth', 'created_at', 'updated_at',
76
+ ];
77
+
78
+ /**
79
+ * Recreate the sessions table with the given column SQL, preserving existing data.
80
+ * @param {import('better-sqlite3').Database} db
81
+ * @param {string} columnsSql
82
+ * @param {string[]} allColumnNames
83
+ */
84
+ export function recreateSessionsTable(db, columnsSql, allColumnNames) {
85
+ const existingColumnNames = getColumns(db, TABLE_SESSIONS);
86
+ const selectColumns = allColumnNames
87
+ .filter((col) => existingColumnNames.includes(col))
88
+ .join(', ');
89
+
90
+ const foreignKeysEnabled = db.pragma('foreign_keys', { simple: true });
91
+ db.pragma('foreign_keys = OFF');
92
+
93
+ try {
94
+ db.exec(`
95
+ CREATE TABLE sessions_new (${columnsSql});
96
+ INSERT INTO sessions_new (${selectColumns})
97
+ SELECT ${selectColumns} FROM sessions;
98
+ DROP TABLE sessions;
99
+ ALTER TABLE sessions_new RENAME TO sessions;
100
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
101
+ CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
102
+ CREATE INDEX IF NOT EXISTS idx_sessions_archived ON sessions(archived);
103
+ CREATE INDEX IF NOT EXISTS idx_sessions_starred ON sessions(archived, starred);
104
+ CREATE INDEX IF NOT EXISTS idx_sessions_next_template ON sessions(next_template_id);
105
+ CREATE INDEX IF NOT EXISTS idx_sessions_parent ON sessions(parent_session_id);
106
+ CREATE INDEX IF NOT EXISTS idx_sessions_scheduled ON sessions(scheduled_at) WHERE scheduled_at IS NOT NULL;
107
+ `);
108
+
109
+ const foreignKeyViolations = db.pragma('foreign_key_check');
110
+ if (foreignKeyViolations.length > 0) {
111
+ throw new Error('sessions table migration failed foreign key check');
112
+ }
113
+ } finally {
114
+ db.pragma(`foreign_keys = ${foreignKeysEnabled ? 'ON' : 'OFF'}`);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Migrate sessions table defaults: mode → 'yolo', thinking_enabled → 1.
120
+ * No-op if the table already has the target defaults.
121
+ * @param {import('better-sqlite3').Database} db
122
+ */
123
+ export function migrateSessionsDefaultModeAndThinking(db) {
124
+ const columns = db.prepare(`PRAGMA table_info(${TABLE_SESSIONS})`).all();
125
+ const modeColumn = columns.find((col) => col.name === 'mode');
126
+ const thinkingEnabledColumn = columns.find((col) => col.name === 'thinking_enabled');
127
+
128
+ if (
129
+ modeColumn?.dflt_value === SESSIONS_TARGET_MODE_DEFAULT
130
+ && thinkingEnabledColumn?.dflt_value === SESSIONS_TARGET_THINKING_ENABLED_DEFAULT
131
+ ) {
132
+ return;
133
+ }
134
+
135
+ recreateSessionsTable(db, SESSIONS_ALL_CURRENT_COLUMNS, SESSIONS_ALL_CURRENT_COLUMN_NAMES);
136
+ }
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Migrations for the sessions table and closely related session tables.
3
+ * Each export is an array of { name, up(db) } migration objects.
4
+ */
5
+ import { addColumnIfMissing, getColumns, getTableSql } from './migrationUtils.js';
6
+ import { migrateSessionsDefaultModeAndThinking } from './sessionTableRecreate.js';
7
+
8
+ // Table name constants for migrations
9
+ const TABLE_SESSIONS = 'sessions';
10
+
11
+ // Column type constants
12
+ const COL_INTEGER_DEFAULT_0 = 'INTEGER DEFAULT 0';
13
+
14
+ /**
15
+ * SQL column definition for the sessions table with updated status CHECK constraint.
16
+ */
17
+ const SESSIONS_BASE_COLUMNS = `
18
+ id TEXT PRIMARY KEY,
19
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
20
+ name TEXT NOT NULL,
21
+ status TEXT NOT NULL DEFAULT 'starting' CHECK (status IN ('starting', 'running', 'waiting', 'stopped', 'completed', 'error', 'scheduled')),
22
+ mode TEXT NOT NULL DEFAULT 'yolo' CHECK (mode IN ('plan', 'standard', 'yolo')),
23
+ thinking_enabled INTEGER NOT NULL DEFAULT 1,
24
+ git_branch TEXT,
25
+ git_worktree TEXT,
26
+ pr_url TEXT,
27
+ pr_url_auto_link_disabled INTEGER NOT NULL DEFAULT 0,
28
+ error TEXT,
29
+ effort_level TEXT CHECK(effort_level IN ('low', 'medium', 'high', 'max', 'auto')),
30
+ cost_usd REAL DEFAULT 0,
31
+ claude_session_id TEXT,
32
+ model TEXT,
33
+ next_template_id TEXT REFERENCES session_templates(id) ON DELETE SET NULL,
34
+ parent_session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL,
35
+ input_tokens INTEGER DEFAULT 0,
36
+ output_tokens INTEGER DEFAULT 0,
37
+ thinking_tokens INTEGER DEFAULT 0,
38
+ cache_read_input_tokens INTEGER DEFAULT 0,
39
+ cache_creation_input_tokens INTEGER DEFAULT 0,
40
+ web_search_requests INTEGER DEFAULT 0,
41
+ context_window INTEGER DEFAULT 200000,
42
+ archived INTEGER NOT NULL DEFAULT 0,
43
+ starred INTEGER NOT NULL DEFAULT 0,
44
+ manually_named INTEGER NOT NULL DEFAULT 0,
45
+ scheduled_at INTEGER DEFAULT NULL,
46
+ reschedule_delay_minutes INTEGER DEFAULT 60, -- keep in sync with DEFAULT_RESCHEDULE_DELAY_MINUTES in shared/constants.js
47
+ auto_reschedule_enabled INTEGER DEFAULT 0,
48
+ reschedule_on_token_limit INTEGER DEFAULT 1,
49
+ reschedule_on_service_error INTEGER DEFAULT 1,
50
+ max_reschedule_count INTEGER DEFAULT NULL,
51
+ max_total_tokens INTEGER DEFAULT NULL,
52
+ reschedule_count INTEGER DEFAULT 0,
53
+ reschedule_at_token_count INTEGER DEFAULT NULL,
54
+ created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
55
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
56
+ `;
57
+
58
+ /**
59
+ * All possible column names that may exist in the sessions table for migration SELECT.
60
+ */
61
+ const SESSIONS_ALL_COLUMNS = [
62
+ 'id', 'project_id', 'name', 'status', 'mode', 'thinking_enabled',
63
+ 'git_branch', 'git_worktree', 'pr_url', 'pr_url_auto_link_disabled', 'error', 'effort_level',
64
+ 'cost_usd', 'claude_session_id', 'model', 'next_template_id',
65
+ 'parent_session_id', 'input_tokens', 'output_tokens', 'thinking_tokens',
66
+ 'cache_read_input_tokens', 'cache_creation_input_tokens',
67
+ 'web_search_requests', 'context_window', 'archived', 'starred',
68
+ 'manually_named', 'scheduled_at', 'reschedule_delay_minutes',
69
+ 'auto_reschedule_enabled', 'reschedule_on_token_limit',
70
+ 'reschedule_on_service_error', 'max_reschedule_count',
71
+ 'max_total_tokens', 'reschedule_count', 'reschedule_at_token_count',
72
+ 'created_at', 'updated_at',
73
+ ];
74
+
75
+ /**
76
+ * Migrate sessions table to include 'stopped' and 'scheduled' in status CHECK constraint.
77
+ * SQLite doesn't support ALTER TABLE to modify constraints, so we recreate the table.
78
+ */
79
+ function migrateSessionsStatusConstraint(db) {
80
+ const tableSql = getTableSql(db, TABLE_SESSIONS);
81
+
82
+ // If schema already includes 'scheduled', no migration needed
83
+ if (tableSql?.includes("'scheduled'")) {
84
+ return;
85
+ }
86
+
87
+ const columnNames = getColumns(db, TABLE_SESSIONS);
88
+ const selectColumns = SESSIONS_ALL_COLUMNS
89
+ .filter((col) => columnNames.includes(col))
90
+ .join(', ');
91
+
92
+ db.exec(`
93
+ CREATE TABLE sessions_new (${SESSIONS_BASE_COLUMNS});
94
+ INSERT INTO sessions_new (${selectColumns})
95
+ SELECT ${selectColumns} FROM sessions;
96
+ DROP TABLE sessions;
97
+ ALTER TABLE sessions_new RENAME TO sessions;
98
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
99
+ CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
100
+ CREATE INDEX IF NOT EXISTS idx_sessions_archived ON sessions(archived);
101
+ CREATE INDEX IF NOT EXISTS idx_sessions_starred ON sessions(archived, starred);
102
+ CREATE INDEX IF NOT EXISTS idx_sessions_next_template ON sessions(next_template_id);
103
+ CREATE INDEX IF NOT EXISTS idx_sessions_parent ON sessions(parent_session_id);
104
+ CREATE INDEX IF NOT EXISTS idx_sessions_scheduled ON sessions(scheduled_at) WHERE scheduled_at IS NOT NULL;
105
+ `);
106
+ }
107
+
108
+ /** @type {Array<{name: string, up: (db: import('better-sqlite3').Database) => void}>} */
109
+ export const sessionsMigrations = [
110
+ // --- Initial sessions columns ---
111
+ {
112
+ name: 'sessions-add-cost_usd',
113
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'cost_usd', 'REAL DEFAULT 0'); },
114
+ },
115
+ {
116
+ name: 'sessions-add-claude_session_id',
117
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'claude_session_id', 'TEXT'); },
118
+ },
119
+ {
120
+ name: 'sessions-add-model',
121
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'model', 'TEXT'); },
122
+ },
123
+ {
124
+ name: 'sessions-add-provider_id-early',
125
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'provider_id', 'TEXT'); },
126
+ },
127
+ {
128
+ name: 'sessions-add-effort_level',
129
+ up(db) {
130
+ addColumnIfMissing(
131
+ db, TABLE_SESSIONS, 'effort_level',
132
+ "TEXT CHECK(effort_level IN ('low', 'medium', 'high', 'max', 'auto'))"
133
+ );
134
+ },
135
+ },
136
+
137
+ // --- Scheduling columns ---
138
+ {
139
+ name: 'sessions-add-scheduled_at',
140
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'scheduled_at', 'INTEGER DEFAULT NULL'); },
141
+ },
142
+ {
143
+ name: 'sessions-add-reschedule_delay_minutes',
144
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'reschedule_delay_minutes', 'INTEGER DEFAULT 60'); /* keep in sync with DEFAULT_RESCHEDULE_DELAY_MINUTES */ },
145
+ },
146
+ {
147
+ name: 'sessions-add-auto_reschedule_enabled',
148
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'auto_reschedule_enabled', COL_INTEGER_DEFAULT_0); },
149
+ },
150
+ {
151
+ name: 'sessions-add-reschedule_on_token_limit',
152
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'reschedule_on_token_limit', 'INTEGER DEFAULT 1'); },
153
+ },
154
+ {
155
+ name: 'sessions-add-reschedule_on_service_error',
156
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'reschedule_on_service_error', 'INTEGER DEFAULT 1'); },
157
+ },
158
+ {
159
+ name: 'sessions-add-max_reschedule_count',
160
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'max_reschedule_count', 'INTEGER DEFAULT NULL'); },
161
+ },
162
+ {
163
+ name: 'sessions-add-max_total_tokens',
164
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'max_total_tokens', 'INTEGER DEFAULT NULL'); },
165
+ },
166
+ {
167
+ name: 'sessions-add-reschedule_count',
168
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'reschedule_count', COL_INTEGER_DEFAULT_0); },
169
+ },
170
+ {
171
+ name: 'sessions-add-reschedule_at_token_count',
172
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'reschedule_at_token_count', 'INTEGER DEFAULT NULL'); },
173
+ },
174
+
175
+ // --- Status constraint migration (table recreation) ---
176
+ {
177
+ name: 'sessions-migrate-status-constraint',
178
+ up(db) { migrateSessionsStatusConstraint(db); },
179
+ },
180
+
181
+ // --- Template chaining ---
182
+ {
183
+ name: 'sessions-add-next_template_id',
184
+ up(db) {
185
+ addColumnIfMissing(
186
+ db, TABLE_SESSIONS, 'next_template_id',
187
+ 'TEXT REFERENCES session_templates(id) ON DELETE SET NULL'
188
+ );
189
+ },
190
+ },
191
+ {
192
+ name: 'sessions-add-parent_session_id',
193
+ up(db) {
194
+ addColumnIfMissing(
195
+ db, TABLE_SESSIONS, 'parent_session_id',
196
+ 'TEXT REFERENCES sessions(id) ON DELETE SET NULL'
197
+ );
198
+ },
199
+ },
200
+ {
201
+ name: 'sessions-template-chaining-indexes',
202
+ up(db) {
203
+ db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_next_template ON sessions(next_template_id)');
204
+ db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_parent ON sessions(parent_session_id)');
205
+ },
206
+ },
207
+
208
+ // --- Token usage columns ---
209
+ {
210
+ name: 'sessions-add-input_tokens',
211
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'input_tokens', COL_INTEGER_DEFAULT_0); },
212
+ },
213
+ {
214
+ name: 'sessions-add-output_tokens',
215
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'output_tokens', COL_INTEGER_DEFAULT_0); },
216
+ },
217
+ {
218
+ name: 'sessions-add-thinking_tokens',
219
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'thinking_tokens', COL_INTEGER_DEFAULT_0); },
220
+ },
221
+ {
222
+ name: 'sessions-add-cache_read_input_tokens',
223
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'cache_read_input_tokens', COL_INTEGER_DEFAULT_0); },
224
+ },
225
+ {
226
+ name: 'sessions-add-cache_creation_input_tokens',
227
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'cache_creation_input_tokens', COL_INTEGER_DEFAULT_0); },
228
+ },
229
+ {
230
+ name: 'sessions-add-web_search_requests',
231
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'web_search_requests', COL_INTEGER_DEFAULT_0); },
232
+ },
233
+ {
234
+ name: 'sessions-add-context_window',
235
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'context_window', 'INTEGER DEFAULT 200000'); },
236
+ },
237
+
238
+ // --- Archived / starred / manually_named ---
239
+ {
240
+ name: 'sessions-add-archived',
241
+ up(db) {
242
+ addColumnIfMissing(db, TABLE_SESSIONS, 'archived', 'INTEGER NOT NULL DEFAULT 0');
243
+ db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_archived ON sessions(archived)');
244
+ },
245
+ },
246
+ {
247
+ name: 'sessions-add-starred',
248
+ up(db) {
249
+ addColumnIfMissing(db, TABLE_SESSIONS, 'starred', 'INTEGER NOT NULL DEFAULT 0');
250
+ db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_starred ON sessions(archived, starred)');
251
+ },
252
+ },
253
+ {
254
+ name: 'sessions-add-manually_named',
255
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'manually_named', 'INTEGER NOT NULL DEFAULT 0'); },
256
+ },
257
+ {
258
+ name: 'sessions-add-pr_url_auto_link_disabled',
259
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'pr_url_auto_link_disabled', 'INTEGER NOT NULL DEFAULT 0'); },
260
+ },
261
+
262
+ // --- Pending prompt / slash commands / pending model / auto send ---
263
+ {
264
+ name: 'sessions-add-pending_prompt',
265
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'pending_prompt', 'TEXT'); },
266
+ },
267
+ {
268
+ name: 'sessions-add-slash_commands',
269
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'slash_commands', 'TEXT'); },
270
+ },
271
+ {
272
+ name: 'sessions-add-pending_model',
273
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'pending_model', 'TEXT'); },
274
+ },
275
+ {
276
+ name: 'sessions-add-auto_send_pending_prompt',
277
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'auto_send_pending_prompt', COL_INTEGER_DEFAULT_0); },
278
+ },
279
+
280
+ // --- Provider ID (from providers table, added later in sequence) ---
281
+ {
282
+ name: 'sessions-add-provider_id-from-providers',
283
+ up(db) {
284
+ addColumnIfMissing(db, TABLE_SESSIONS, 'provider_id', 'TEXT REFERENCES providers(id)');
285
+ },
286
+ },
287
+
288
+ // --- Agent type ---
289
+ {
290
+ name: 'sessions-add-agent_type',
291
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'agent_type', "TEXT DEFAULT 'claude-code'"); },
292
+ },
293
+
294
+ // --- Default mode / thinking changes ---
295
+ {
296
+ name: 'sessions-migrate-default-mode-thinking',
297
+ up(db) { migrateSessionsDefaultModeAndThinking(db); },
298
+ },
299
+
300
+ ];
@@ -7,7 +7,32 @@ import { DEFAULT_RESCHEDULE_DELAY_MINUTES } from '../../../shared/src/index.js';
7
7
 
8
8
  /** Reusable SQL fragment for computed activity fields on sessions */
9
9
  export const ACTIVITY_FIELDS_SQL = `
10
- (SELECT MAX(cm.timestamp) FROM conversation_messages cm WHERE cm.session_id = s.id) AS last_activity_at,
10
+ (
11
+ SELECT MAX(activity_at)
12
+ FROM (
13
+ SELECT cm.timestamp AS activity_at
14
+ FROM conversation_messages cm
15
+ WHERE cm.session_id = s.id
16
+ UNION ALL
17
+ SELECT ss.generated_at AS activity_at
18
+ FROM session_summaries ss
19
+ WHERE ss.session_id = s.id
20
+ UNION ALL
21
+ SELECT ss.updated_at AS activity_at
22
+ FROM session_summaries ss
23
+ WHERE ss.session_id = s.id
24
+ UNION ALL
25
+ SELECT COALESCE(cr.completed_at, cr.started_at) AS activity_at
26
+ FROM command_runs cr
27
+ WHERE cr.session_id = s.id
28
+ )
29
+ WHERE activity_at IS NOT NULL
30
+ ) AS last_activity_at,
31
+ (
32
+ SELECT MAX(cm.timestamp)
33
+ FROM conversation_messages cm
34
+ WHERE cm.session_id = s.id
35
+ ) AS last_message_at,
11
36
  (CAST(
12
37
  CASE
13
38
  WHEN (SELECT MAX(cm2.timestamp) FROM conversation_messages cm2 WHERE cm2.session_id = s.id) IS NOT NULL
@@ -26,6 +26,10 @@ CREATE TABLE IF NOT EXISTS session_templates (
26
26
  mode TEXT DEFAULT 'yolo' CHECK(mode IN ('plan', 'standard', 'yolo')),
27
27
  effort_level TEXT CHECK(effort_level IN ('low', 'medium', 'high', 'max', 'auto')),
28
28
  target_lane_id TEXT REFERENCES kanban_lanes(id) ON DELETE SET NULL,
29
+ show_in_quick_responses INTEGER NOT NULL DEFAULT 0,
30
+ quick_response_auto_submit INTEGER NOT NULL DEFAULT 0,
31
+ quick_response_sort_order INTEGER NOT NULL DEFAULT 0,
32
+ legacy_quick_response_id TEXT UNIQUE,
29
33
  created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
30
34
  updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
31
35
  );
@@ -13,36 +13,38 @@ export function buildCommandButtonApiInstructions(apiUrl, sessionId, projectId)
13
13
  return '';
14
14
  }
15
15
 
16
- return `## Commands API
16
+ return `## Circus Commands
17
17
 
18
- This project has commands configured - reusable shell commands you can execute. Use the Bash tool to run these curl commands.
18
+ This project has Circus Commands configured - reusable shell commands you can execute. Use the Bash tool to run these curl commands.
19
+
20
+ > When the user asks to "run a command", "what commands are available", "list circus commands", or similar, use the Commands API below to discover and execute them.
19
21
 
20
22
  ### List Available Commands
21
23
  \`\`\`bash
22
- curl ${apiUrl}/api/sessions/${sessionId}/command-buttons
24
+ curl ${apiUrl}/api/sessions/${sessionId}/circus-commands
23
25
  \`\`\`
24
26
 
25
27
  ### Run a Command
26
28
  \`\`\`bash
27
- curl -X POST ${apiUrl}/api/sessions/${sessionId}/command-buttons/<button_id>/run
29
+ curl -X POST ${apiUrl}/api/sessions/${sessionId}/circus-commands/<button_id>/run
28
30
  \`\`\`
29
31
 
30
32
  Response: { runId, buttonId, status: "running", output: "" }
31
33
 
32
34
  ### Check Run Status & Output
33
35
  \`\`\`bash
34
- curl ${apiUrl}/api/sessions/${sessionId}/command-buttons/runs/<run_id>
36
+ curl ${apiUrl}/api/sessions/${sessionId}/circus-commands/runs/<run_id>
35
37
  \`\`\`
36
38
 
37
39
  Response: { runId, buttonId, status, exitCode, output, startedAt, completedAt }
38
40
 
39
41
  ### List Command Runs
40
42
  \`\`\`bash
41
- curl ${apiUrl}/api/sessions/${sessionId}/command-buttons/runs
43
+ curl ${apiUrl}/api/sessions/${sessionId}/circus-commands/runs
42
44
  \`\`\`
43
45
 
44
46
  ### Kill a Running Command
45
47
  \`\`\`bash
46
- curl -X POST ${apiUrl}/api/sessions/${sessionId}/command-buttons/runs/<run_id>/kill
48
+ curl -X POST ${apiUrl}/api/sessions/${sessionId}/circus-commands/runs/<run_id>/kill
47
49
  \`\`\``;
48
50
  }
@@ -1,10 +1,38 @@
1
1
  import { exec } from 'child_process';
2
2
  import { promisify } from 'util';
3
+ import os from 'os';
3
4
  import path from 'path';
4
5
  import { chmod, mkdir, writeFile } from 'fs/promises';
5
6
 
6
7
  const execAsync = promisify(exec);
7
- const MANAGED_HOOKS_PATH = '.circuschief-hooks';
8
+
9
+ const DEFAULT_MANAGED_HOOKS_PATH = path.join(os.homedir(), '.circuschief', 'hooks');
10
+ let _managedHooksPath = DEFAULT_MANAGED_HOOKS_PATH;
11
+
12
+ /**
13
+ * Get the managed hooks directory path.
14
+ * Production code uses the real home directory; tests can override via _setManagedHooksPath().
15
+ * @returns {string}
16
+ */
17
+ export function getManagedHooksPath() {
18
+ return _managedHooksPath;
19
+ }
20
+
21
+ /**
22
+ * Override the managed hooks path (for testing only).
23
+ * Restores the default when called with no arguments.
24
+ * @param {string} [overridePath]
25
+ */
26
+ export function _setManagedHooksPath(overridePath) {
27
+ _managedHooksPath = overridePath ?? DEFAULT_MANAGED_HOOKS_PATH;
28
+ }
29
+
30
+ /**
31
+ * Legacy managed hooks path (pre-migration). Used by ensureWorktreeCommitAttributionHook()
32
+ * to recognize and auto-upgrade worktrees that still have the old relative path set.
33
+ */
34
+ const LEGACY_MANAGED_HOOKS_PATH = '.circuschief-hooks';
35
+
8
36
  const ATTRIBUTION_CONFIG_KEY = 'circuschief.commitAttribution';
9
37
  const ATTRIBUTION_ENV_KEY = 'CIRCUSCHIEF_COMMIT_ATTRIBUTION';
10
38
 
@@ -54,9 +82,10 @@ git interpret-trailers --trailer "$trailer" --in-place "$msg_file"
54
82
  }
55
83
 
56
84
  export async function clearWorktreeCommitAttribution(worktreePath) {
85
+ const managedHooksPath = getManagedHooksPath();
57
86
  const currentAttribution = await gitConfigValue(worktreePath, ATTRIBUTION_CONFIG_KEY);
58
87
  const currentHooksPath = await gitConfigValue(worktreePath, 'core.hooksPath');
59
- if (!currentAttribution && currentHooksPath !== MANAGED_HOOKS_PATH) {
88
+ if (!currentAttribution && currentHooksPath !== managedHooksPath) {
60
89
  return false;
61
90
  }
62
91
 
@@ -70,7 +99,7 @@ export async function clearWorktreeCommitAttribution(worktreePath) {
70
99
  }
71
100
  }
72
101
 
73
- if (currentHooksPath === MANAGED_HOOKS_PATH) {
102
+ if (currentHooksPath === managedHooksPath) {
74
103
  try {
75
104
  await git(worktreePath, 'config --worktree --unset core.hooksPath');
76
105
  } catch {
@@ -90,10 +119,12 @@ export async function clearWorktreeCommitAttribution(worktreePath) {
90
119
  * @returns {Promise<boolean>} True when a hook is installed or updated
91
120
  */
92
121
  export async function ensureWorktreeCommitAttributionHook(worktreePath) {
122
+ const managedHooksPath = getManagedHooksPath();
123
+
93
124
  await git(worktreePath, 'config extensions.worktreeConfig true');
94
125
 
95
126
  const currentHooksPath = await gitConfigValue(worktreePath, 'core.hooksPath');
96
- if (currentHooksPath && currentHooksPath !== MANAGED_HOOKS_PATH) {
127
+ if (currentHooksPath && currentHooksPath !== managedHooksPath && currentHooksPath !== LEGACY_MANAGED_HOOKS_PATH) {
97
128
  throw new Error(
98
129
  `Cannot install managed commit attribution hook: worktree already has core.hooksPath set to "${currentHooksPath}"`
99
130
  );
@@ -105,11 +136,10 @@ export async function ensureWorktreeCommitAttributionHook(worktreePath) {
105
136
  // Unset is idempotent for stale worktrees that never stored attribution.
106
137
  }
107
138
 
108
- await git(worktreePath, `config --worktree core.hooksPath ${shellQuote(MANAGED_HOOKS_PATH)}`);
139
+ await git(worktreePath, `config --worktree core.hooksPath ${shellQuote(managedHooksPath)}`);
109
140
 
110
- const hooksDir = path.join(worktreePath, MANAGED_HOOKS_PATH);
111
- const hookPath = path.join(hooksDir, 'commit-msg');
112
- await mkdir(hooksDir, { recursive: true });
141
+ const hookPath = path.join(managedHooksPath, 'commit-msg');
142
+ await mkdir(managedHooksPath, { recursive: true });
113
143
  await writeFile(hookPath, buildCommitMsgHook(), 'utf8');
114
144
  await chmod(hookPath, 0o755);
115
145
  return true;