instar 0.9.0 → 0.9.2

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 (59) hide show
  1. package/dist/cli.js +0 -0
  2. package/dist/commands/server.d.ts.map +1 -1
  3. package/dist/commands/server.js +202 -71
  4. package/dist/commands/server.js.map +1 -1
  5. package/dist/commands/setup.d.ts.map +1 -1
  6. package/dist/commands/setup.js +38 -4
  7. package/dist/commands/setup.js.map +1 -1
  8. package/dist/core/AgentConnector.d.ts +76 -0
  9. package/dist/core/AgentConnector.d.ts.map +1 -0
  10. package/dist/core/AgentConnector.js +323 -0
  11. package/dist/core/AgentConnector.js.map +1 -0
  12. package/dist/core/AutoUpdater.d.ts +7 -0
  13. package/dist/core/AutoUpdater.d.ts.map +1 -1
  14. package/dist/core/AutoUpdater.js +31 -3
  15. package/dist/core/AutoUpdater.js.map +1 -1
  16. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  17. package/dist/core/PostUpdateMigrator.js +86 -5
  18. package/dist/core/PostUpdateMigrator.js.map +1 -1
  19. package/dist/core/StateWriteAuthority.d.ts +101 -0
  20. package/dist/core/StateWriteAuthority.d.ts.map +1 -0
  21. package/dist/core/StateWriteAuthority.js +167 -0
  22. package/dist/core/StateWriteAuthority.js.map +1 -0
  23. package/dist/core/types.d.ts +104 -0
  24. package/dist/core/types.d.ts.map +1 -1
  25. package/dist/index.d.ts +2 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +1 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/memory/TopicMemory.d.ts +167 -0
  30. package/dist/memory/TopicMemory.d.ts.map +1 -0
  31. package/dist/memory/TopicMemory.js +494 -0
  32. package/dist/memory/TopicMemory.js.map +1 -0
  33. package/dist/memory/TopicSummarizer.d.ts +58 -0
  34. package/dist/memory/TopicSummarizer.d.ts.map +1 -0
  35. package/dist/memory/TopicSummarizer.js +140 -0
  36. package/dist/memory/TopicSummarizer.js.map +1 -0
  37. package/dist/messaging/TelegramAdapter.d.ts +35 -0
  38. package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
  39. package/dist/messaging/TelegramAdapter.js +136 -2
  40. package/dist/messaging/TelegramAdapter.js.map +1 -1
  41. package/dist/server/AgentServer.d.ts +2 -0
  42. package/dist/server/AgentServer.d.ts.map +1 -1
  43. package/dist/server/AgentServer.js +1 -0
  44. package/dist/server/AgentServer.js.map +1 -1
  45. package/dist/server/routes.d.ts +2 -0
  46. package/dist/server/routes.d.ts.map +1 -1
  47. package/dist/server/routes.js +340 -1
  48. package/dist/server/routes.js.map +1 -1
  49. package/dist/users/UserManager.d.ts +21 -0
  50. package/dist/users/UserManager.d.ts.map +1 -1
  51. package/dist/users/UserManager.js +32 -0
  52. package/dist/users/UserManager.js.map +1 -1
  53. package/dist/users/UserOnboarding.d.ts +116 -0
  54. package/dist/users/UserOnboarding.d.ts.map +1 -0
  55. package/dist/users/UserOnboarding.js +365 -0
  56. package/dist/users/UserOnboarding.js.map +1 -0
  57. package/package.json +2 -1
  58. package/upgrades/0.8.23.md +106 -0
  59. package/upgrades/0.9.1.md +91 -0
@@ -11,6 +11,7 @@ import fs from 'node:fs';
11
11
  import os from 'node:os';
12
12
  import path from 'node:path';
13
13
  import { rateLimiter, signViewPath } from './middleware.js';
14
+ import { validateWriteToken, canPerformOperation } from '../core/StateWriteAuthority.js';
14
15
  // Validation patterns for route parameters
15
16
  const SESSION_NAME_RE = /^[a-zA-Z0-9_-]{1,200}$/;
16
17
  const JOB_SLUG_RE = /^[a-zA-Z0-9_-]{1,100}$/;
@@ -431,6 +432,18 @@ export function createRoutes(ctx) {
431
432
  users: {
432
433
  count: userCount,
433
434
  },
435
+ topicMemory: {
436
+ enabled: !!ctx.topicMemory,
437
+ stats: ctx.topicMemory?.stats() ?? null,
438
+ endpoints: ctx.topicMemory ? [
439
+ 'GET /topic/search?q=...&topic=N&limit=20',
440
+ 'GET /topic/context/:topicId?recent=30',
441
+ 'GET /topic/list',
442
+ 'GET /topic/stats',
443
+ 'POST /topic/summarize { topicId }',
444
+ 'POST /topic/summary { topicId, summary, messageCount, lastMessageId }',
445
+ ] : [],
446
+ },
434
447
  monitoring: ctx.config.monitoring,
435
448
  evolution: {
436
449
  enabled: !!ctx.evolution,
@@ -460,7 +473,7 @@ export function createRoutes(ctx) {
460
473
  { context: 'User has a recurring task', action: 'Create a scheduled job in .instar/jobs.json. Explain it will run automatically.' },
461
474
  { context: 'User repeats a workflow', action: 'Create a skill in .claude/skills/. It becomes a slash command for future sessions.' },
462
475
  { context: 'User is debugging CI or deployment', action: 'Check CI health (GET /ci) for GitHub Actions status.' },
463
- { context: 'User asks about past events', action: 'Search Telegram history (GET /telegram/search?q=...), check memory, review activity logs.' },
476
+ { context: 'User asks about past events or prior conversations', action: 'Search topic memory (GET /topic/search?q=...), get topic context (GET /topic/context/:topicId), check memory, review activity logs.' },
464
477
  { context: 'User frustrated with a limitation', action: 'Check for updates (GET /updates). Check dispatches (GET /dispatches/pending). The fix may already exist.' },
465
478
  { context: 'User asks to remember something', action: 'Write to .instar/MEMORY.md. Explain it persists across sessions.' },
466
479
  { context: 'Something needs user attention later', action: 'Queue in attention system (POST /attention). More reliable than hoping they see a message.' },
@@ -1947,6 +1960,332 @@ export function createRoutes(ctx) {
1947
1960
  ctx.watchdog.setEnabled(enabled);
1948
1961
  res.json({ enabled: ctx.watchdog.isEnabled() });
1949
1962
  });
1963
+ // ── Topic Memory (conversation search & context) ─────────────────────
1964
+ /**
1965
+ * Search topic message history with FTS5 full-text search.
1966
+ * GET /topic/search?q=query&topic=topicId&limit=20
1967
+ */
1968
+ router.get('/topic/search', (req, res) => {
1969
+ if (!ctx.topicMemory) {
1970
+ res.status(503).json({ error: 'TopicMemory not initialized' });
1971
+ return;
1972
+ }
1973
+ const q = (req.query.q || '').trim();
1974
+ if (!q) {
1975
+ res.status(400).json({ error: 'q (search query) required' });
1976
+ return;
1977
+ }
1978
+ const topicId = req.query.topic ? parseInt(req.query.topic, 10) : undefined;
1979
+ const limit = Math.min(parseInt(req.query.limit, 10) || 20, 100);
1980
+ const results = ctx.topicMemory.search(q, { topicId, limit });
1981
+ res.json({ query: q, topicId: topicId ?? null, results, totalResults: results.length });
1982
+ });
1983
+ /**
1984
+ * Get full context for a topic (summary + recent messages).
1985
+ * GET /topic/context/:topicId?recent=30
1986
+ */
1987
+ router.get('/topic/context/:topicId', (req, res) => {
1988
+ if (!ctx.topicMemory) {
1989
+ res.status(503).json({ error: 'TopicMemory not initialized' });
1990
+ return;
1991
+ }
1992
+ const topicId = parseInt(req.params.topicId, 10);
1993
+ if (isNaN(topicId)) {
1994
+ res.status(400).json({ error: 'Invalid topicId' });
1995
+ return;
1996
+ }
1997
+ const recentLimit = Math.min(parseInt(req.query.recent, 10) || 30, 100);
1998
+ const context = ctx.topicMemory.getTopicContext(topicId, recentLimit);
1999
+ res.json(context);
2000
+ });
2001
+ /**
2002
+ * List all topics with metadata.
2003
+ * GET /topic/list
2004
+ */
2005
+ router.get('/topic/list', (_req, res) => {
2006
+ if (!ctx.topicMemory) {
2007
+ res.status(503).json({ error: 'TopicMemory not initialized' });
2008
+ return;
2009
+ }
2010
+ const topics = ctx.topicMemory.listTopics();
2011
+ res.json({ topics, total: topics.length });
2012
+ });
2013
+ /**
2014
+ * Get topic memory stats.
2015
+ * GET /topic/stats
2016
+ */
2017
+ router.get('/topic/stats', (_req, res) => {
2018
+ if (!ctx.topicMemory) {
2019
+ res.status(503).json({ error: 'TopicMemory not initialized' });
2020
+ return;
2021
+ }
2022
+ res.json(ctx.topicMemory.stats());
2023
+ });
2024
+ /**
2025
+ * Trigger summary generation for a topic.
2026
+ * POST /topic/summarize { topicId: number }
2027
+ */
2028
+ router.post('/topic/summarize', (req, res) => {
2029
+ if (!ctx.topicMemory) {
2030
+ res.status(503).json({ error: 'TopicMemory not initialized' });
2031
+ return;
2032
+ }
2033
+ const topicId = req.body?.topicId;
2034
+ if (typeof topicId !== 'number') {
2035
+ res.status(400).json({ error: 'topicId (number) required' });
2036
+ return;
2037
+ }
2038
+ const needsUpdate = ctx.topicMemory.needsSummaryUpdate(topicId, 1);
2039
+ const messagesSince = ctx.topicMemory.getMessagesSinceSummary(topicId);
2040
+ const currentSummary = ctx.topicMemory.getTopicSummary(topicId);
2041
+ // Return the data needed for an LLM to generate the summary.
2042
+ // The actual LLM call happens in the calling session (not in the HTTP handler).
2043
+ res.json({
2044
+ topicId,
2045
+ needsUpdate,
2046
+ currentSummary: currentSummary?.summary ?? null,
2047
+ messagesSinceSummary: messagesSince.length,
2048
+ messages: messagesSince.map(m => ({
2049
+ from: m.fromUser ? 'User' : 'Agent',
2050
+ text: m.text,
2051
+ timestamp: m.timestamp,
2052
+ messageId: m.messageId,
2053
+ })),
2054
+ });
2055
+ });
2056
+ /**
2057
+ * Save a generated summary for a topic.
2058
+ * POST /topic/summary { topicId, summary, messageCount, lastMessageId }
2059
+ */
2060
+ router.post('/topic/summary', (req, res) => {
2061
+ if (!ctx.topicMemory) {
2062
+ res.status(503).json({ error: 'TopicMemory not initialized' });
2063
+ return;
2064
+ }
2065
+ const { topicId, summary, messageCount, lastMessageId } = req.body || {};
2066
+ if (typeof topicId !== 'number' || typeof summary !== 'string') {
2067
+ res.status(400).json({ error: 'topicId (number) and summary (string) required' });
2068
+ return;
2069
+ }
2070
+ ctx.topicMemory.saveTopicSummary(topicId, summary, messageCount ?? 0, lastMessageId ?? 0);
2071
+ res.json({ saved: true, topicId });
2072
+ });
2073
+ /**
2074
+ * Rebuild topic memory from JSONL (idempotent import).
2075
+ * POST /topic/rebuild
2076
+ */
2077
+ router.post('/topic/rebuild', (_req, res) => {
2078
+ if (!ctx.topicMemory) {
2079
+ res.status(503).json({ error: 'TopicMemory not initialized' });
2080
+ return;
2081
+ }
2082
+ const jsonlPath = path.join(ctx.config.stateDir, 'telegram-messages.jsonl');
2083
+ const imported = ctx.topicMemory.rebuild(jsonlPath);
2084
+ res.json({ rebuilt: true, messagesImported: imported, stats: ctx.topicMemory.stats() });
2085
+ });
2086
+ // ── Pairing API — Multi-machine state sync (Phase 4.5) ────────
2087
+ /**
2088
+ * POST /state/submit — Secondary machine submits a state change.
2089
+ * Validates write token, checks operation authorization, applies or queues.
2090
+ */
2091
+ router.post('/state/submit', (req, res) => {
2092
+ const { operation, payload, machineId, writeToken } = req.body || {};
2093
+ // Validate required fields
2094
+ if (!operation || !payload || !machineId || !writeToken) {
2095
+ res.status(400).json({
2096
+ error: 'Missing required fields: operation, payload, machineId, writeToken',
2097
+ });
2098
+ return;
2099
+ }
2100
+ if (typeof operation !== 'string' || typeof machineId !== 'string' || typeof writeToken !== 'string') {
2101
+ res.status(400).json({ error: 'operation, machineId, and writeToken must be strings' });
2102
+ return;
2103
+ }
2104
+ // Load stored write tokens
2105
+ const tokensFile = path.join(ctx.config.stateDir, 'write-tokens.json');
2106
+ let storedTokens = [];
2107
+ try {
2108
+ if (fs.existsSync(tokensFile)) {
2109
+ storedTokens = JSON.parse(fs.readFileSync(tokensFile, 'utf-8'));
2110
+ }
2111
+ }
2112
+ catch {
2113
+ res.status(500).json({ error: 'Failed to load write tokens' });
2114
+ return;
2115
+ }
2116
+ // Validate the write token
2117
+ const tokenResult = validateWriteToken(writeToken, storedTokens);
2118
+ if (!tokenResult.valid) {
2119
+ res.status(403).json({ error: tokenResult.error });
2120
+ return;
2121
+ }
2122
+ // Verify the token was issued to the claiming machine
2123
+ if (tokenResult.machineId !== machineId) {
2124
+ res.status(403).json({ error: 'Write token does not match machineId' });
2125
+ return;
2126
+ }
2127
+ // Check if the operation is allowed
2128
+ const opCheck = canPerformOperation(operation);
2129
+ if (!opCheck.allowed) {
2130
+ res.status(403).json({
2131
+ error: opCheck.reason,
2132
+ requiresConfirmation: opCheck.requiresConfirmation,
2133
+ });
2134
+ return;
2135
+ }
2136
+ // Apply the state change based on operation type
2137
+ try {
2138
+ switch (operation) {
2139
+ case 'addMemory': {
2140
+ // Append memory entry to memories.jsonl
2141
+ const memoriesFile = path.join(ctx.config.stateDir, 'memories.jsonl');
2142
+ const entry = { ...payload, sourceMachineId: machineId, appliedAt: new Date().toISOString() };
2143
+ fs.appendFileSync(memoriesFile, JSON.stringify(entry) + '\n');
2144
+ res.json({ applied: true, operation });
2145
+ break;
2146
+ }
2147
+ case 'updateProfile': {
2148
+ // Update a user profile field
2149
+ const usersFile = path.join(ctx.config.stateDir, 'users.json');
2150
+ if (!fs.existsSync(usersFile)) {
2151
+ res.status(404).json({ error: 'No users file found' });
2152
+ return;
2153
+ }
2154
+ const users = JSON.parse(fs.readFileSync(usersFile, 'utf-8'));
2155
+ const targetUser = users.find((u) => u.id === payload.userId);
2156
+ if (!targetUser) {
2157
+ res.status(404).json({ error: `User ${payload.userId} not found` });
2158
+ return;
2159
+ }
2160
+ // Apply the update fields (shallow merge)
2161
+ if (payload.updates && typeof payload.updates === 'object') {
2162
+ Object.assign(targetUser, payload.updates);
2163
+ }
2164
+ fs.writeFileSync(usersFile, JSON.stringify(users, null, 2));
2165
+ res.json({ applied: true, operation, userId: payload.userId });
2166
+ break;
2167
+ }
2168
+ case 'heartbeat': {
2169
+ // Heartbeat is handled by the dedicated endpoint below
2170
+ res.json({ applied: true, operation });
2171
+ break;
2172
+ }
2173
+ default: {
2174
+ res.status(400).json({ error: `Unknown operation: ${operation}` });
2175
+ }
2176
+ }
2177
+ }
2178
+ catch (err) {
2179
+ res.status(500).json({
2180
+ error: 'Failed to apply state change',
2181
+ detail: err instanceof Error ? err.message : String(err),
2182
+ });
2183
+ }
2184
+ });
2185
+ /**
2186
+ * GET /state/sync — Secondary machine pulls latest state.
2187
+ * Returns current users, config summary, and machine registry.
2188
+ */
2189
+ router.get('/state/sync', (_req, res) => {
2190
+ try {
2191
+ // Read users
2192
+ const usersFile = path.join(ctx.config.stateDir, 'users.json');
2193
+ let users = [];
2194
+ if (fs.existsSync(usersFile)) {
2195
+ try {
2196
+ users = JSON.parse(fs.readFileSync(usersFile, 'utf-8'));
2197
+ }
2198
+ catch { /* empty array on corruption */ }
2199
+ }
2200
+ // Read machine registry
2201
+ const registryFile = path.join(ctx.config.stateDir, 'machine-registry.json');
2202
+ let machineRegistry = { version: 1, machines: {} };
2203
+ if (fs.existsSync(registryFile)) {
2204
+ try {
2205
+ machineRegistry = JSON.parse(fs.readFileSync(registryFile, 'utf-8'));
2206
+ }
2207
+ catch { /* default on corruption */ }
2208
+ }
2209
+ // Config summary (non-sensitive fields only)
2210
+ const configSummary = {
2211
+ projectName: ctx.config.projectName,
2212
+ userRegistrationPolicy: ctx.config.userRegistrationPolicy ?? 'admin-only',
2213
+ agentAutonomy: ctx.config.agentAutonomy?.level ?? 'supervised',
2214
+ multiMachine: ctx.config.multiMachine ?? { enabled: false },
2215
+ userCount: users.length,
2216
+ };
2217
+ res.json({
2218
+ users,
2219
+ machineRegistry,
2220
+ configSummary,
2221
+ syncedAt: new Date().toISOString(),
2222
+ });
2223
+ }
2224
+ catch (err) {
2225
+ res.status(500).json({
2226
+ error: 'Failed to sync state',
2227
+ detail: err instanceof Error ? err.message : String(err),
2228
+ });
2229
+ }
2230
+ });
2231
+ /**
2232
+ * POST /state/heartbeat — Secondary machine reports online status.
2233
+ * Updates lastSeen for the machine and returns queued change count.
2234
+ */
2235
+ router.post('/state/heartbeat', (req, res) => {
2236
+ const { machineId } = req.body || {};
2237
+ if (!machineId || typeof machineId !== 'string') {
2238
+ res.status(400).json({ error: 'machineId (string) required' });
2239
+ return;
2240
+ }
2241
+ try {
2242
+ // Update machine lastSeen in registry
2243
+ const registryFile = path.join(ctx.config.stateDir, 'machine-registry.json');
2244
+ let registry = {
2245
+ version: 1,
2246
+ machines: {},
2247
+ };
2248
+ if (fs.existsSync(registryFile)) {
2249
+ try {
2250
+ registry = JSON.parse(fs.readFileSync(registryFile, 'utf-8'));
2251
+ }
2252
+ catch { /* use default */ }
2253
+ }
2254
+ if (registry.machines[machineId]) {
2255
+ registry.machines[machineId].lastSeen = new Date().toISOString();
2256
+ fs.writeFileSync(registryFile, JSON.stringify(registry, null, 2));
2257
+ }
2258
+ // Count queued changes for this machine (from offline queue if it exists)
2259
+ const queueFile = path.join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.instar', 'offline-queue', `${ctx.config.projectName}.jsonl`);
2260
+ let queuedChanges = 0;
2261
+ if (fs.existsSync(queueFile)) {
2262
+ const content = fs.readFileSync(queueFile, 'utf-8').trim();
2263
+ if (content) {
2264
+ queuedChanges = content.split('\n').filter(line => {
2265
+ try {
2266
+ const entry = JSON.parse(line);
2267
+ return entry.sourceMachineId === machineId;
2268
+ }
2269
+ catch {
2270
+ return false;
2271
+ }
2272
+ }).length;
2273
+ }
2274
+ }
2275
+ res.json({
2276
+ status: 'ok',
2277
+ machineId,
2278
+ queuedChanges,
2279
+ timestamp: new Date().toISOString(),
2280
+ });
2281
+ }
2282
+ catch (err) {
2283
+ res.status(500).json({
2284
+ error: 'Heartbeat processing failed',
2285
+ detail: err instanceof Error ? err.message : String(err),
2286
+ });
2287
+ }
2288
+ });
1950
2289
  return router;
1951
2290
  }
1952
2291
  export function formatUptime(ms) {